diff options
author | Philip Paquette <pcpaquette@gmail.com> | 2018-09-26 07:48:55 -0400 |
---|---|---|
committer | Philip Paquette <pcpaquette@gmail.com> | 2019-04-18 11:14:24 -0400 |
commit | 6187faf20384b0c5a4966343b2d4ca47f8b11e45 (patch) | |
tree | 151ccd21aea20180432c13fe4b58240d3d9e98b6 /diplomacy | |
parent | 96b7e2c03ed98705754f13ae8efa808b948ee3a8 (diff) |
Release v1.0.0 - Diplomacy Game Engine - AGPL v3+ License
Diffstat (limited to 'diplomacy')
149 files changed, 50519 insertions, 0 deletions
diff --git a/diplomacy/README_COMMANDS.txt b/diplomacy/README_COMMANDS.txt new file mode 100644 index 0000000..50eda5b --- /dev/null +++ b/diplomacy/README_COMMANDS.txt @@ -0,0 +1,56 @@ +// Comments +// ----------------------------- +// Order Syntax +// ----------------------------- +// *** Recommended syntax ** +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 + +// *** Other possible syntax ** +// Hold +A LON H +PAR H + +// Move +F IRI - MAO +F IRI - MAO VIA +IRI - MAO +IRI - MAO - XXX - YYY - ZZZ + +// Support +A WAL S F LON +WAL S LON +A WAL S LON +F WAL S F MAO - IRI + +// Convoy +F NWG C A NWY - EDI +NWG C NWY - EDI + +// Retreat (Retreat Phase) +A IRO R MAO +RETREAT IRO - MAO +A IRO D +RETREAT IRO DISBAND + +// Build +A LON B +F LIV B +BUILD A LON +BUILD F LIV + +// Remove +F LIV D +REMOVE F LIV +DISBAND F LIV + +// Waive +WAIVE diff --git a/diplomacy/README_MAPS.txt b/diplomacy/README_MAPS.txt new file mode 100644 index 0000000..59eff65 --- /dev/null +++ b/diplomacy/README_MAPS.txt @@ -0,0 +1,248 @@ +Map File Syntax +*************************** + +The engine already supports a number of existing map variants, each of which is implemented +by a .map file in the format described here. + +*************************** +General Directives +*************************** + +>>> # comment... + +Any line that has a pound sign (#) as its first non-whitespace character will be considered +a comment, and ignored. Comments must span a complete line (that is, you may not add a comment +to the end of a non-comment line). + +>>> USE[S] fileName... + +This line causes another file to be immediately read in. This is used to share map information. +For example, the britain.map file USES the standard.geography file, which contains all the +geography for the standard Diplomacy map. + +>>> MAP mapName + +This line is identical to USE mapName.map with the additional provision that the graphical +map template (used to generate the actual visual map) will be the one having the mapName +specified. For example, the "fleetRome" map not only USEs the same map data (slightly altered) +as the standard map, but also uses the same graphical map for display (and therefore, the MAP +command is proper for that variant). By contrast, the "britain" and "crowded" maps may USE +(that is, load and then alter) the data from the standard map file, but (due to the additional +SC dots that must be depicted on the map to support these variants), they use different graphical +maps than does the standard game, so the USE command (rather than MAP) is proper for these variants. + +*************************** +Geographic Directives +*************************** + +>>> placeName = abbreviation alias... + +This type of line specifies all the recognized aliases for a map location. The placeName +is the long form of the map location (the form to appear in results mailings). The abbreviation +is the engine standard abbreviation for the place-name (the form to appear in all orders when +shown to the player on his Web pages). This standard abbreviation must be exactly three +characters long and must both begin and end with an alphabetic or numeric character. Each +alias is a single word (that is, having no embedded spaces) that is to be recognized as +another name for the location in question. If you wish to specify an alias that is more +than a single word in length, you must join the separate words using plus-sign characters (+). +For example, Norwegian Sea = nwg norwegian nrg norwsea norw+sea + +If an alias ends with a question-mark (?), it must not contain a plus-sign, and this indicates +that the alias (without the question-mark) is to be considered "ambiguous." Ambiguous aliases +may not be used in games that use the NO_CHECK rule (the ambiguity will be reported to the +player at the time of order-entry). For example, TYR is an ambiguous alias in the standard +game map, since it is commonly used as an alias for both Tyrolia and the Tyrrhenian Sea. + +>>> COAST abbreviation [ABUTS [abut...]] -or- +>>> LAND abbreviation [ABUTS [abut...]] -or- +>>> WATER abbreviation [ABUTS [abut...]] -or- +>>> PORT abbreviation [ABUTS [abut...]] -or- +>>> SHUT abbreviation [ABUTS [abut...]] -or- + +This type of line specifies the terrain type and adjacencies for the place-name whose +abbreviation is given. The terrain must be either WATER, LAND, COAST, PORT, or SHUT (impassable). +The only difference between PORT and COAST spaces is that fleets located in PORT spaces may convoy +armies as if they were in water. + +The adjacency information given on this line will silently supplant (become valid instead of) any +previous such line for the same location. In this way, maps can be changed on the fly, such that +what was a LAND location can become COAST or WATER and/or can obtain different adjacencies. + +The abut locations must be the standard abbreviations (the first abbreviation given after +the equals-sign in each placename line). + +Specifying Army and Fleet Restrictions +=========================== +This directive is case-sensitive. Army and fleet restrictions are specifyied by use of upper- and +lower-case as described below. Everything must be in upper-case except the following: + +The abbreviation for any COAST location that a fleet cannot occupy must be listed entirely in +lowercase. For instance, on the standard map, Spain must be listed as spa since fleets may only +occupy either SPA/SC or SPA/NC. Specifying a location using lower-case after the same location +was given earlier using upper-case, or vice-versa, will cause the previous location's terrain type +and adjacencies to be forgotten. + +In the list of adjacencies, any location that a fleet could occupy but into which a fleet cannot +move from the location in question must be listed entirely in lowercase. For example, on the standard +map, Tuscany must be listed as tus in the list of locations adjacent to VEN, since a fleet cannot +move directly from Venice to Tuscany. + +In the list of adjacencies, any LAND or COAST location to which an army cannot make a direct +(non-convoyed) move from the location in question must be listed with its first character in uppercase +and the remainder in lower-case. This is useful to specify offshore islands, and therefore no example +on the standard map can be given. Consider, however, allowing an army to be convoyed into the +Tyrrhenian Sea (call it Sicily). To implement this, the Tyrrhenian Sea would be given the terrain type +PORT (to allow both fleets and armies to occupy the space and to allow fleets in that space to convoy) +and all land spaces adjacent to the Tyrrhenian Sea would list their adjacency to that space as Tys +(rather than TYS or tys) to indicate than an army may not move directly to the space despite the fact +that movement from a COAST (such as Naples) to an adjacent PORT (the Tyrrhenian) would otherwise be +allowed. Convoyed movement would be allowed, though, so an order such as A Nap-ION-TYS would be perfectly +valid. + +Specifying Multi-Coast Spaces +=========================== +If an area abuts a multi-coast province, its adjacencies must list only the coasts that are reachable, +and must not list the main space itself (for example, RUM is listed as being adjacent to BUL/EC, but +not to BUL itself). + +The line for every coast of a province should appear in the map file before the line for the space +itself. + +If the map will undergo terrain changes during play, it is important to note that the fleet-friendly +space listed last is the one on which a unit in that space will be put if a terrain modification +requires it. For example, if a fleet is in Rome when Venice becomes a WATER space, the map file will +need to direct that Rome becomes a multi-coast province by adding entries for ROM/EC, ROM/WC, and rom. +By listing ROM/WC after ROM/EC, the fleet in Rome will be placed on the west (rather than the east) coast. + +>>> DROP abbreviation... + +This line will remove all data that was given concerning the place(s) with the specified +abbreviation(s), including all adjacency information. That is, all record of the space will be +forgotten, and no space will be left thinking that it is adjacent to the DROPped location. DROPping +a multi-coast space, without designating any particular coast, will also DROP all information for each +of the coasts. That is, DROP SPA, used on the standard map, will remove all information on Spain, Spain +(north coast), and Spain (south coast). + +*************************** +Political Directives +*************************** + +>>> powerName [([ownWord][:[abbrev]])] [center...] + +This type of line is used to specify a power name, its "ownership word" and single-letter +abbreviation (for instance, England's ownership word would be "English", and abbreviation would be "E") +and all centers that serve as the home centers for the power. If the ownWord is omitted, the powerName +is used for this purpose, and if the abbrev is omitted, the initial letter of the ownWord is used as +the abbreviation. + +Any and all leading underscores (_) in a powerName will not be displayed when the power is referred +to in results and on the graphical map. This is useful to cause powers to alphabetize after others +(and thus be displayed in a specific order in the results and on the graphical map). + +A plus-sign (+) appearing anywhere in the powerName indicates that the next character in the name +is to be displayed in upper-case. Any plus-sign appearing in the ownWord will be displayed as a space. + +If the center is preceded (immediately, with no intervening space) by a dash (-), the center is +removed (if possible) from any pre-existing list of home centers for the power, and added to the list +of unowned supply centers. This allows for locations to lose "home center" status. Factory and partisan +sites may also be removed in this same manner. + +Multiple lines for a single powerName may be used to handle long lists of centers. + +This type of line sets a "current power" such that all following lines (that specify initial owned +centers and units) will apply to this power (as opposed to any other) until another powerName directive +is encountered. + +>>> UNOWNED [center...] -or- +>>> NEUTRAL [center...] -or- +>>> CENTERS [center...] + +This line (all three forms are synonymous) is used to list all unowned supply centers. The +UNOWNED power differs from others in that all centers listed as UNOWNED may be listed elsewhere +without error -- they are silently moved to owned status. Additionally, any "current power" is +forgotten by this line. Again, multiple lines may be used to supply a long list of unowned centers. +If you remove a center from the list of UNOWNED centers (using a dash before its name), that location +is no longer a supply center at all. For example, to make Warsaw a non-supply center on the standard +map, you would need to specify the two lines: "RUSSIA -WAR" and "UNOWNED -WAR" in that order. + +>>> DUMMY [ALL] -or- +>>> DUMMY [ALL EXCEPT] powerName... -or- +>>> DUMMIES ALL -or- +>>> DUMMIES [ALL EXCEPT] powerName... + +This indicates that either the current power, or all powers, or only those specified by powerName, +or again all powers other than those specified, are to be made a DUMMY power in the game. No player +will take the role of such a power. Such a power can be put into civil disorder using the CD_DUMMIES +rule. + +>>> UNPLAYED [ALL] -or- +>>> UNPLAYED [ALL EXCEPT] powerName... + +This line removes either the current power, all powers, the specified powerName, or on the +contrary all powers that are not specified. At the same time all information that had been +recorded for those powers is removed as well. This directive is useful to create variants for +a smaller number of players on a specific USEd map. If you remove all powers, you should add +some new powers afterwards, because a game will not start with less than two powers. + +*************************** +Military Directives +*************************** + +>>> VICTORY centerCount... + +Specifies a list of the supply center counts which will determine victory, from the first +game-year forward (the final listed number is repeated for subsequent years). This line is +optional; if omitted, the VICTORY criterion is set (for all game-years) to one supply center +greater than half the number of centers on the map, unless the VICTORY_HOMES rule is used, in +which case the default is based on the number of home centers. + +>>> BEGIN season year phaseType + +Specifies the initial game phase. This line is optional; if it is omitted, the initial +game phase will be SPRING 1901 MOVEMENT. Only the final BEGIN listed in the map file will +be used. This allows for map files to USE other map files and then override the start phase. + +>>> OWNS center... + +Specifies the list of centers owned by the "current power" at the beginning of the game. +If not specified, the list of initially owned centers is set to the home centers that were +listed for the "current power." + +>>> CENTERS [center...] + +Same as OWNS, but forgets about any previous OWNS lines. If no center is given, the power +will start with no centers at all (if not followed by any new OWNS lines that is). + +>>> INHABITS center... + +Sets the home centers for this power, overwriting any that are given on the power declaration +line, including partisan and factory sites. + +>>> HOME(S) [center...] + +Same as INHABITS, but forgets about any previous INHABITS lines. If no center is given, the +power will start with no build sites at all (if not followed by any new INHABITS lines that is). + +>>> UNITS + +This line is optional. If given, all units that have been listed as initial units for the +"current power" are immediately forgotten. This can be used to alter the starting position +of a power from that given in a USEd file. + +>>> unit + +Specifies a unit that the current power owns at the beginning of the game. The unit will +replace any unit that may have previously been listed as beginning in that same space, + +*************************** +Behavioral Directives +*************************** +>>> RULE[S] rule... + +Specifies a rule that will be in effect for all games using this map. For example, +RULE BUILD_ANY would appear in a map file for the Chaos variant, since that variant allows players +to build new units in any unoccupied, owned supply center. If this line occurs outside of a +DIRECTIVE stanza, it will be in effect for all games using this map, regardless of any rules-variant +(payola, etc.) that the game may use. Note that if any rule name is prepended by exclamation point (!), +that rule will be turned off if possible, and this will be done after all other RULEs have been +processed. diff --git a/diplomacy/README_RULES.txt b/diplomacy/README_RULES.txt new file mode 100644 index 0000000..4839fe5 --- /dev/null +++ b/diplomacy/README_RULES.txt @@ -0,0 +1,250 @@ +! indicates that rule is forbidden +- indicates that rule will be removed += indicates that rule is implied ++ indicates that rule will be added + +================================================================== +** POWER_CHOICE ** () + If this rule is used, players may select an (unspoken for) power to play + when JOINing the game. (Without this rule, the DPjudge will assign powers + to all players randomly.) + +================================================================== +** MULTIPLE_POWERS_PER_PLAYER ** () + If this rule is used, each player is allowed to control more than 1 power in the game. + +================================================================== +** NO_OBSERVATIONS ** () + If this rule is used, no observers are allowed for the game. Note that game masters + can still observe the game as omniscient observers even if this rule is present. + +================================================================== +** SOLITAIRE ** (+NO_DEADLINE +CD_DUMMIES +ALWAYS_WAIT) + A SOLITAIRE game is a game where all powers are dummies controlled by + the Master. This can be used to test a certain scenario or to solve a + puzzle on the Diplomacy board, such as the Sherlock Holmes riddles published + in the Diplomatic Pouch Zine. A PRIVACY password will be automatically + assigned, because these are typically private games. After allowing the + game to form, all powers will become dummies and the game starts immediately. + The FORMING phase simply gets skipped. Make sure to have all relevant + rules in place during preparation or when creating the game. + + The following rules are automatically included: + - NO_DEADLINE + - CD_DUMMIES + - ALWAYS_WAIT + + To advance a turn the Master needs to push the Process Turn button + or send a PROCESS command by e-mail. Use ROLLBACK and ROLLFORWARD to + undo and redo phases, either by e-mail or from the corresponding + Results page. Note that the Master CONTROLs all powers, and thus can + give orders to each of them in a single message. + +================================================================== +** START_MASTER ** () + This rule prevents the game from automatically starting once the last player + joined. The Master will need to change the game status to active to begin the + game. + +================================================================== +** NO_PRESS ** (-PUBLIC_PRESS) + Press is only allowed to and from the Master. This rule is invalid + when used with PUBLIC_PRESS or other rules that imply that any type + of press is allowed. + +================================================================== +** PUBLIC_PRESS ** (-NO_PRESS) + Press is only allowed to and from the Master, and broadcast. + (See the descriptions of the TOUCH_PRESS and REMOTE_PRESS rules for other + implications of the PUBLIC_PRESS rule.) + +================================================================== +** DONT_SKIP_PHASES ** () + If this rule is used, all phases (including phases where all players need + to issue blank orders) will be played, and empty phases won't be skipped. + +================================================================== +** IGNORE_ERRORS ** () + Order errors will be silently ignored. + +================================================================== +** BUILD_ANY ** () + Powers may build new units at any owned supply center, not simply at + their home supply centers. + +================================================================== +** CIVIL_DISORDER ** (+CD_DUMMIES) + Any power that has not submitted orders: + + - when the game's Master submits a PROCESS command, or + + - when the deadline has passed (or, if a grace period is + specified in the TIMING line of the game's + status file, after the expiration of + the grace period), + + will have its orders entered automatically. During movement phases, all + units will HOLD, during retreat phases, all dislodged units will DISBAND, + and during adjustment phases, all builds will be WAIVED, and all removals + will be entered using random choice from among the power's on-board units, + with preference given to retaining units that are situated on supply centers. + + Note also that unless the LATE_CHANGES rule is specified, the CIVIL_DISORDER + rule also enforces the NO_LATE_CHANGES rule (described below). This is to + prevent players from changing their orders after the deadline to take advantage + of powers that seem likely to go into civil disorder when a grace period expires. + + Individual powers may also be put into perpetual automatic, don't-even-wait-for-the-deadline, + civil disorder (assuming no one like the Master enters orders for them) + by setting the PLAYER information to DUMMY, and using the + CD_DUMMIES rule, described below. + +================================================================== +** CD_DUMMIES ** () + Assuming no powers have set their WAIT flag, orders will be + processed as <i><u>soon</u></i> as all NON-DUMMY + players have submitted orders. Any powers having a PLAYER marked + as a DUMMY will be considered in civil disorder (as described + above) at that time. Note the distinction -- CIVIL_DISORDER + will default the orders of all powers (whether dummy or not), while + CD_DUMMIES will default the orders only of the dummy powers. + +================================================================== +** NO_DEADLINE ** () + In certain cases, e.g. for testing and solving Diplomacy puzzles (solitaire games), + there's no need for a deadline, and thus also not for a Timing line. It's up to the + GM to process each turn after all (relevant) orders are submitted, or for all players + to submit their orders without setting their WAIT flag. This behavior can be + influenced with the NO_WAIT and ALWAYS_WAIT options. + +================================================================== +** REAL_TIME ** (!ALWAYS_WAIT) + If this rule is used, the game will be processed AS SOON AS + the last player submits orders (this player will have no opportunity + to modify them). Additionally, players will not be allowed to direct + that the game wait for any deadline before processing. This rule is + used especially for games with very short deadlines (for example, + 10 minutes). + +================================================================== +** ALWAYS_WAIT ** (!REAL_TIME) + If this rule is used, orders will never process until the deadline + has arrived (unless requested by the Master using a PROCESS + command sent via e-mail). This rule is incompatible with + REAL_TIME and restricted to movement phases if NO_MINOR_WAIT is selected. + +================================================================== +** PROPOSE_DIAS ** (!NO_DIAS) + In games that use this rule, a player makes a proposal to conclude the + game with a certain result, and all other players vote on it. + The proposal may be a concession to a specific power or a draw including + all survivors. Any negative vote on the proposal will cause it to fail. + + In games NOT using this rule, draw and concession votes are never + called for. Instead, each player can select a single game ending + (concession to his own power or to any other single power, or agreement to a + draw that includes all surviving powers), and if ever all votes allow, + the game will end. No player's vote is revealed to any other player. Note + that if all players are ever found to be simultaneously voting for any + result that is NOT a concession to their own power, the game will + end in a draw shared among all surviving powers. + +================================================================== +** NO_DIAS ** (!PROPOSE_DIAS) + Games with this rule operate in their voting as do normal + (non-PROPOSE_DIAS) games; that is, no proposal is ever made or + vetoed. However, games using the NO_DIAS rule may end in a + result other than concession to a single player or agreement to a draw + that includes all survivors. If this rule is used, each player may vote + either for the maximum size of a draw that he will accept which must + include his power, or for a solo victory by his power, or for + ANY result that does NOT include his power. Note that if all + players are ever found to be voting for a result that does + NOT include their own power, a draw shared among all surviving + powers will be declared. + +================================================================== +** HOLD_WIN ** () + To win a game using this rule, a player must achieve the winning condition + two game-years in a row. + +================================================================== +** SHARED_VICTORY ** () + When this rule is in effect, the game ends immediately after the first + player reaches the victory condition. If any other player fulfills this + condition at the same time (in games where this number is lower than + the default of half of the number of SCs plus one), they are jointly + declared winners (or participants in a draw, depending on definitions), + irrespective of the fact that one may have a higher total than the + other. This replaces the normal victory criterion where only a + single player can be victorious and ties result in the continuation + of the game. + +================================================================== +******************************************************* +** VARIANT standard ** +******************************************************* + +** NO_CHECK ** () + This rule emulates face-to-face play, in which players could (by + accident or design) issue invalid orders to their units. This rule + is also useful in NO_PRESS games to allow for limited player + communication (see SIGNAL_SUPPORT for a more controlled alternative). + + When they are entered, movement phase orders are only very minimally + checked for validity. The ONLY checks that are made at the time an + order is entered are: + + - Every component of the order must be understood. That is, the + order must appear to be a Diplomacy move, convoy, support or + hold order, and all placenames must be identifiable on the map + in use. This check catches inadvertant misspellings, such as "URK" for "UKR". + In fact, this is known as the "Urk check." + + - Any placename abbreviation that is potentially ambiguous is declared + erroneous and must be changed. For example, the order "TYR H" is rejected + because it may be an order for an army in Tyrolia to hold, or for a + fleet in the Tyrrhenian Sea to hold. + + - A support for a fleet move may not specify the destination coast of + the fleet. This error must also be corrected. + + Therefore, most errors (including the omission of the fleet-path of + a convoying army from its order!) are not detected until the phase + is ready to process, at which time the erroneous orders will be ignored. + All units that had been given erroneous or multiple orders will + HOLD (and may receive support), and all erroneous orders will + be reported in the results, flagged as (*invalid*). + +================================================================== + +<!-- RULE GROUP 0 Game Start --> +<!-- RULE POWER_CHOICE --> +<!-- RULE MULTIPLE_POWERS_PER_PLAYER --> +<!-- RULE NO_OBSERVATIONS --> +<!-- RULE SOLITAIRE +NO_DEADLINE +CD_DUMMIES +ALWAYS_WAIT --> +<!-- RULE START_MASTER --> +<!-- RULE GROUP 2 Player Press --> +<!-- RULE NO_PRESS -PUBLIC_PRESS --> +<!-- RULE PUBLIC_PRESS -NO_PRESS --> +<!-- RULE GROUP 3 Movement Order --> +<!-- RULE DONT_SKIP_PHASES --> +<!-- RULE IGNORE_ERRORS --> +<!-- RULE GROUP 5 Retreat and Adjustment Order --> +<!-- RULE BUILD_ANY --> +<!-- RULE GROUP 7 Late and Vacant Power Handling --> +<!-- RULE CIVIL_DISORDER +CD_DUMMIES --> +<!-- RULE CD_DUMMIES --> +<!-- RULE GROUP 9 Deadline Handling and Phase Processing --> +<!-- RULE NO_DEADLINE --> +<!-- RULE REAL_TIME !ALWAYS_WAIT --> +<!-- RULE ALWAYS_WAIT !REAL_TIME --> +<!-- RULE GROUP 10 Game Conclusion --> +<!-- RULE PROPOSE_DIAS !NO_DIAS --> +<!-- RULE NO_DIAS !PROPOSE_DIAS --> +<!-- RULE HOLD_WIN --> +<!-- RULE SHARED_VICTORY --> +<!-- RULE VARIANT standard --> +<!-- RULE GROUP 3 Movement Order --> +<!-- RULE NO_CHECK --> +<!-- RULE DIFFERENT_ADJUDICATION --> diff --git a/diplomacy/__init__.py b/diplomacy/__init__.py new file mode 100644 index 0000000..585d52d --- /dev/null +++ b/diplomacy/__init__.py @@ -0,0 +1,47 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Module diplomacy, represent strategy game Diplomacy. """ +import logging +import os +import coloredlogs +from .engine.map import Map +from .engine.power import Power +from .engine.game import Game +from .engine.message import Message +from .client.connection import Connection, connect +from .server.server import Server +from .utils.game_phase_data import GamePhaseData + +# Defining root logger +ROOT = logging.getLogger('diplomacy') +ROOT.setLevel(logging.DEBUG) +ROOT.propagate = False + +STREAM_HANDLER = logging.StreamHandler() +STREAM_HANDLER.setLevel(logging.DEBUG) +COLORED_FORMATTER = coloredlogs.ColoredFormatter(fmt='%(asctime)s %(name)s[%(process)d] %(levelname)s %(message)s') +STREAM_HANDLER.setFormatter(COLORED_FORMATTER) +ROOT.addHandler(STREAM_HANDLER) + +if 'DIPLOMACY_LOG_FILE' in os.environ: + LOG_FILE_NAME = os.environ['DIPLOMACY_LOG_FILE'] + ROOT.info('Logging into file: %s', LOG_FILE_NAME) + FILE_HANDLER = logging.FileHandler(LOG_FILE_NAME) + FILE_HANDLER.setLevel(logging.DEBUG) + LOG_FILE_FORMATTER = logging.Formatter(fmt='%(asctime)s %(name)s[%(process)d] %(levelname)s %(message)s') + FILE_HANDLER.setFormatter(LOG_FILE_FORMATTER) + ROOT.addHandler(FILE_HANDLER) diff --git a/diplomacy/client/__init__.py b/diplomacy/client/__init__.py new file mode 100644 index 0000000..4f2769f --- /dev/null +++ b/diplomacy/client/__init__.py @@ -0,0 +1,16 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== diff --git a/diplomacy/client/channel.py b/diplomacy/client/channel.py new file mode 100644 index 0000000..0403e7f --- /dev/null +++ b/diplomacy/client/channel.py @@ -0,0 +1,156 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Channel + - The channel object represents an authenticated connection over a socket. + - It has a token that it sends with every request to authenticate itself. +""" +import logging + +from tornado import gen + +from diplomacy.communication import requests +from diplomacy.utils import strings + +LOGGER = logging.getLogger(__name__) + +def req_fn(request_class, local_req_fn=None, **request_args): + """ Create channel request method that sends request with channel token. + :param request_class: class of request to send with channel request method. + :param local_req_fn: (optional) Channel method to use locally to try retrieving a data + instead of sending a request. If provided, local_req_fn is called with request args: + - if it returns anything else than None, then returned data is returned by channel request method. + - else, request class is still sent and channel request method follows standard path + (request sent, response received, response handler called and final handler result returned). + :param request_args: arguments to pass to request class to create the request object. + :return: a Channel method. + """ + + @gen.coroutine + def func(self, game_object=None, **kwargs): + """ Send an instance of request_class with given kwargs and game object. + :param self: Channel object who sends the request. + :param game_object: (optional) a NetworkGame object (required for game requests). + :param kwargs: request arguments. + :return: Data returned after response is received and handled by associated response manager. + See module diplomacy.client.response_managers about responses management. + :type game_object: diplomacy.client.network_game.NetworkGame + """ + kwargs.update(request_args) + if request_class.level == strings.GAME: + assert game_object is not None + kwargs[strings.TOKEN] = self.token + kwargs[strings.GAME_ID] = game_object.game_id + kwargs[strings.GAME_ROLE] = game_object.role + kwargs[strings.PHASE] = game_object.current_short_phase + else: + assert game_object is None + if request_class.level == strings.CHANNEL: + kwargs[strings.TOKEN] = self.token + if local_req_fn is not None: + local_ret = local_req_fn(self, **kwargs) + if local_ret is not None: + return local_ret + request = request_class(**kwargs) + return (yield self.connection.send(request, game_object)) + + return func + +class Channel(): + """ Channel - Represents an authenticated connection over a physical socket """ + __slots__ = ['connection', 'token', 'game_id_to_instances', '__weakref__'] + + def __init__(self, connection, token): + """ Initialize a channel. + :param connection: a Connection object. + :param token: Channel token. + :type connection: diplomacy.Connection + """ + self.connection = connection + self.token = token + self.game_id_to_instances = {} # {game id => GameInstances} + + def local_join_game(self, **kwargs): + """ Look for a local game with given kwargs intended to be used to build a JoinGame request. + Return None if no local game found, else local game found. + Game is identified with game ID and power name (optional). + If power name is None, we look for a "special" game (observer or omniscient game) + loaded locally. Note that there is at most 1 special game per channel + game ID: + either observer or omniscient, not both. + """ + game_id = kwargs[strings.GAME_ID] + power_name = kwargs.get(strings.POWER_NAME, None) + if game_id in self.game_id_to_instances: + if power_name is not None: + return self.game_id_to_instances[game_id].get(power_name) + return self.game_id_to_instances[game_id].get_special() + return None + + # =================== + # Public channel API. + # =================== + + create_game = req_fn(requests.CreateGame) + get_available_maps = req_fn(requests.GetAvailableMaps) + get_playable_powers = req_fn(requests.GetPlayablePowers) + join_game = req_fn(requests.JoinGame, local_req_fn=local_join_game) + join_powers = req_fn(requests.JoinPowers) + list_games = req_fn(requests.ListGames) + get_games_info = req_fn(requests.GetGamesInfo) + + # User Account API. + delete_account = req_fn(requests.DeleteAccount) + logout = req_fn(requests.Logout) + + # Admin / Moderator API. + make_omniscient = req_fn(requests.SetGrade, grade=strings.OMNISCIENT, grade_update=strings.PROMOTE) + remove_omniscient = req_fn(requests.SetGrade, grade=strings.OMNISCIENT, grade_update=strings.DEMOTE) + promote_administrator = req_fn(requests.SetGrade, grade=strings.ADMIN, grade_update=strings.PROMOTE) + demote_administrator = req_fn(requests.SetGrade, grade=strings.ADMIN, grade_update=strings.DEMOTE) + promote_moderator = req_fn(requests.SetGrade, grade=strings.MODERATOR, grade_update=strings.PROMOTE) + demote_moderator = req_fn(requests.SetGrade, grade=strings.MODERATOR, grade_update=strings.DEMOTE) + + # ================ + # Public game API. + # ================ + + get_dummy_waiting_powers = req_fn(requests.GetDummyWaitingPowers) + get_phase_history = req_fn(requests.GetPhaseHistory) + leave_game = req_fn(requests.LeaveGame) + send_game_message = req_fn(requests.SendGameMessage) + set_orders = req_fn(requests.SetOrders) + + clear_centers = req_fn(requests.ClearCenters) + clear_orders = req_fn(requests.ClearOrders) + clear_units = req_fn(requests.ClearUnits) + + wait = req_fn(requests.SetWaitFlag, wait=True) + no_wait = req_fn(requests.SetWaitFlag, wait=False) + vote = req_fn(requests.Vote) + save = req_fn(requests.SaveGame) + synchronize = req_fn(requests.Synchronize) + + # Admin / Moderator API. + delete_game = req_fn(requests.DeleteGame) + kick_powers = req_fn(requests.SetDummyPowers) + set_state = req_fn(requests.SetGameState) + process = req_fn(requests.ProcessGame) + query_schedule = req_fn(requests.QuerySchedule) + start = req_fn(requests.SetGameStatus, status=strings.ACTIVE) + pause = req_fn(requests.SetGameStatus, status=strings.PAUSED) + resume = req_fn(requests.SetGameStatus, status=strings.ACTIVE) + cancel = req_fn(requests.SetGameStatus, status=strings.CANCELED) + draw = req_fn(requests.SetGameStatus, status=strings.COMPLETED) diff --git a/diplomacy/client/connection.py b/diplomacy/client/connection.py new file mode 100644 index 0000000..f9eb628 --- /dev/null +++ b/diplomacy/client/connection.py @@ -0,0 +1,474 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Connection object, handling an internal websocket tornado connection. """ +import logging +import weakref +from datetime import timedelta + +from tornado import gen, ioloop +from tornado.concurrent import Future +from tornado.iostream import StreamClosedError +from tornado.locks import Event +from tornado.websocket import websocket_connect, WebSocketClosedError + +import ujson as json + +from diplomacy.client import notification_managers +from diplomacy.client.response_managers import RequestFutureContext, handle_response +from diplomacy.communication import notifications, requests, responses +from diplomacy.utils import exceptions, strings, constants + +LOGGER = logging.getLogger(__name__) + +class MessageWrittenCallback(): + """ Helper class representing callback to call on a connection when a request is written in a websocket. """ + __slots__ = ['request_context'] + + def __init__(self, request_context): + """ Initialize the callback object. + :param request_context: a request context + :type request_context: RequestFutureContext + """ + self.request_context = request_context + + def callback(self, msg_future): + """ Called when request is effectively written on socket, and move the request + from `request to send` to `request assumed sent`. + """ + # Remove request context from `requests to send` in any case. + connection = self.request_context.connection # type: Connection + request_id = self.request_context.request_id + exception = msg_future.exception() + if exception is not None: + if isinstance(exception, (WebSocketClosedError, StreamClosedError)): + # Connection suddenly closed. + # Request context was stored in connection.requests_to_send + # and will be re-sent when reconnection succeeds. + # For more details, see method Connection.write_request(). + LOGGER.error('Connection was closed when sending a request. Silently waiting for a reconnection.') + else: + LOGGER.error('Fatal error occurred while writing a request.') + self.request_context.future.set_exception(exception) + else: + connection.requests_waiting_responses[request_id] = self.request_context + +class Reconnection(): + """ Class performing reconnection work for a given connection. + + Class properties: + ================= + + - connection: Connection object to reconnect. + + - games_phases: dictionary mapping each game address (game ID + game role) to server game info: + {game ID => {game role => responses.DataGamePhase}} + Server game info is a DataGamePhase response sent by server as response to a Synchronize request. + It contains 3 fields: game ID, current server game phase and current server game timestamp. + We currently use only game phase. + + - n_expected_games: number of games registered in games_phases. + + - n_synchronized_games: number of games already synchronized. + + Reconnection procedure: + ======================= + + - Mark all waiting responses as `re-sent` (may be useful on server-side) and + move them back to responses_to_send. + + - Remove all previous synchronization requests that are not yet sent. We will send new synchronization + requests with latest games timestamps. Future associated to removed requests will raise an exception. + + - Initialize games_phases associating None to each game object currently opened in connection. + + - Send synchronization request for each game object currently opened in connection. For each game: + + - server will send a response describing current server game phase (current phase and timestamp). This info + will be used to check local requests to send. Note that concrete synchronization is done via notifications. + Thus, when server responses is received, game synchronization may not be yet terminated, but at least + we will now current server game phase. + + - Server response is saved in games_phases (replacing None associated to game object). + + - n_synchronized_games is incremented. + + - When sync responses are received for all games registered in games_phases + (n_expected_games == n_synchronized_games), we can finalize reconnection: + + - Remove every phase-dependent game request not yet sent for which phase does not match + server game phase. Futures associated to removed request will raise an exception. + + - Finally send all remaining requests. + + These requests may be marked as re-sent. + For these requests, server is (currently) responsible for checking if they don't represent + a duplicated query. + + """ + + __slots__ = ['connection', 'games_phases', 'n_expected_games', 'n_synchronized_games'] + + def __init__(self, connection): + """ Initialize reconnection data/ + :param connection: connection to reconnect. + :type connection: Connection + """ + self.connection = connection + self.games_phases = {} + self.n_expected_games = 0 + self.n_synchronized_games = 0 + + def reconnect(self): + """ Perform concrete reconnection work. """ + + # Mark all waiting responses as `re-sent` and move them back to responses_to_send. + for waiting_context in self.connection.requests_waiting_responses.values(): # type: RequestFutureContext + waiting_context.request.re_sent = True + self.connection.requests_to_send.update(self.connection.requests_waiting_responses) + self.connection.requests_waiting_responses.clear() + + # Remove all previous synchronization requests. + requests_to_send_updated = {} + for context in self.connection.requests_to_send.values(): # type: RequestFutureContext + if isinstance(context.request, requests.Synchronize): + context.future.set_exception(exceptions.DiplomacyException( + 'Sync request invalidated for game ID %s.' % context.request.game_id)) + else: + requests_to_send_updated[context.request.request_id] = context + self.connection.requests_to_send = requests_to_send_updated + + # Count games to synchronize. + for channel in self.connection.channels.values(): + for game_instance_set in channel.game_id_to_instances.values(): + for game in game_instance_set.get_games(): + self.games_phases.setdefault(game.game_id, {})[game.role] = None + self.n_expected_games += 1 + + if self.n_expected_games: + # Synchronize games. + for channel in self.connection.channels.values(): + for game_instance_set in channel.game_id_to_instances.values(): + for game in game_instance_set.get_games(): + game.synchronize().add_done_callback(self.generate_sync_callback(game)) + else: + # No game to sync, finish sync now. + self.sync_done() + + def generate_sync_callback(self, game): + """ Generate callback to call when response to sync request is received for given game. + :param game: game + :return: a callback. + :type game: diplomacy.client.network_game.NetworkGame + """ + + def on_sync(future): + """ Callback. If exception occurs, print it as logging error. Else, register server response, + and move forward to final reconnection work if all games received sync responses. + """ + exception = future.exception() + if exception is not None: + LOGGER.error(str(exception)) + else: + self.games_phases[game.game_id][game.role] = future.result() + self.n_synchronized_games += 1 + if self.n_synchronized_games == self.n_expected_games: + self.sync_done() + + return on_sync + + def sync_done(self): + """ Final reconnection work. Remove obsolete game requests and send remaining requests. """ + + # All sync requests sent have finished. + # Remove all obsolete game requests from connection. + # A game request is obsolete if it's phase-dependent and if its phase does not match current game phase. + + request_to_send_updated = {} + for context in self.connection.requests_to_send.values(): # type: RequestFutureContext + keep = True + if context.request.level == strings.GAME and context.request.phase_dependent: + request_phase = context.request.phase + server_phase = self.games_phases[context.request.game_id][context.request.game_role].phase + if request_phase != server_phase: + # Request is obsolete. + context.future.set_exception(exceptions.DiplomacyException( + 'Game %s: request %s: request phase %s does not match current server game phase %s.' + % (context.request.game_id, context.request.name, request_phase, server_phase))) + keep = False + if keep: + request_to_send_updated[context.request.request_id] = context + + LOGGER.debug('Keep %d/%d old requests to send.', + len(request_to_send_updated), len(self.connection.requests_to_send)) + + # All requests to send are stored in request_to_send_updated. + # Then we can empty connection.requests_to_send. + # If we fail to send a request, it will be re-added again. + self.connection.requests_to_send.clear() + + # Send requests. + for request_to_send in request_to_send_updated.values(): # type: RequestFutureContext + self.connection.write_request(request_to_send).add_done_callback( + MessageWrittenCallback(request_to_send).callback) + + # We are reconnected. + self.connection.is_reconnecting.set() + + LOGGER.info('Done reconnection work.') + +class Connection(): + """ Connection class. Properties: + - hostname: hostname to connect (e.g. 'localhost') + - port: port to connect (e.g. 8888) + - use_ssl: boolean telling if connection should be securized (True) or not (False). + - url (auto): websocket url to connect (generated with hostname and port) + - connection: a tornado websocket connection object + - connection_count: number of successful connections from this Connection object. + Used to check if message callbacks is already launched (if count > 0). + - connection_lock: a tornado lock used to access tornado websocket connection object + - is_connecting: a tornado Event used to keep connection status. + No request can be sent while is_connecting. + If connected, Synchronize requests can be sent immediately even if is_reconnecting. + Other requests must wait full reconnection. + - is_reconnecting: a tornado Event used to keep re-connection status. + Non-synchronize request cannot be sent while is_reconnecting. + If reconnected, all requests can be sent. + - channels: a WeakValueDictionary mapping channel token to Channel object. + - requests_to_send: a dictionary mapping a request ID to the context of a request + **not sent**. If we are disconnected when trying to send a request, then request + context is added to this dictionary to be send later once reconnected. + - requests_waiting_responses: a dictionary mapping a request ID to the context of a + request **sent**. Contains requests that are waiting for a server response. + """ + __slots__ = ['hostname', 'port', 'use_ssl', 'connection', 'is_connecting', 'is_reconnecting', 'connection_count', + 'channels', 'requests_to_send', 'requests_waiting_responses'] + + def __init__(self, hostname, port, use_ssl=False): + self.hostname = hostname + self.port = port + self.use_ssl = bool(use_ssl) + + self.connection = None + self.is_connecting = Event() + self.is_reconnecting = Event() + + self.connection_count = 0 + + self.channels = weakref.WeakValueDictionary() # {token => Channel} + + self.requests_to_send = {} # type: dict{str, RequestFutureContext} + self.requests_waiting_responses = {} # type: dict{str, RequestFutureContext} + + # When connection is created, we are not yet connected, but reconnection does not matter + # (we consider we are reconnected). + self.is_reconnecting.set() + + url = property(lambda self: '%s://%s:%d' % ('wss' if self.use_ssl else 'ws', self.hostname, self.port)) + + @gen.coroutine + def _connect(self): + """ Create (force) a tornado websocket connection. Try NB_CONNECTION_ATTEMPTS attempts, + waiting for ATTEMPT_DELAY_SECONDS seconds between 2 attempts. + Raise an exception if it cannot connect. + """ + + # We are connecting. + self.is_connecting.clear() + + # Create a connection (currently using websockets). + self.connection = None + for attempt_index in range(constants.NB_CONNECTION_ATTEMPTS): + try: + future_connection = websocket_connect(self.url) + self.connection = yield gen.with_timeout( + timedelta(seconds=constants.ATTEMPT_DELAY_SECONDS), future_connection) + break + except (gen.TimeoutError, ConnectionAbortedError, ConnectionError, + ConnectionRefusedError, ConnectionResetError) as ex: + if attempt_index + 1 == constants.NB_CONNECTION_ATTEMPTS: + raise ex + LOGGER.warning('Connection failing (attempt %d), retrying.', attempt_index + 1) + yield gen.sleep(constants.ATTEMPT_DELAY_SECONDS) + + if not self.connection_count: + # Start receiving messages as soon as we are connected. + ioloop.IOLoop.current().add_callback(self._handle_socket_messages) + + # We are connected. + self.connection_count += 1 + self.is_connecting.set() + + LOGGER.info('Connection succeeds.') + + @gen.coroutine + def _reconnect(self): + """ Reconnect. """ + LOGGER.info('Trying to reconnect.') + # We are reconnecting. + self.is_reconnecting.clear() + yield self._connect() + # We will be reconnected when method Reconnection.sync_done() will finish. + Reconnection(self).reconnect() + + def _register_to_send(self, request_context): + """ Register given request context as a request to send as soon as possible. + :param request_context: context of request to send. + :type request_context: RequestFutureContext + """ + self.requests_to_send[request_context.request_id] = request_context + + def write_request(self, request_context): + """ Write a request into internal connection object. + :param request_context: context of request to send. + :type request_context: RequestFutureContext + """ + future = Future() + request = request_context.request + + def on_message_written(write_future): + """ 3) Writing returned, set future as done (with writing result) or with writing exception. """ + exception = write_future.exception() + if exception is not None: + future.set_exception(exception) + else: + future.set_result(write_future.result()) + + def on_connected(reconnected_future): + """ 2) Send request. """ + exception = reconnected_future.exception() + if exception is not None: + LOGGER.error('Fatal (re)connection error occurred while sending a request.') + future.set_exception(exception) + else: + try: + if self.connection is None: + raise WebSocketClosedError() + write_future = self.connection.write_message(request.json()) + except (WebSocketClosedError, StreamClosedError) as exc: + # We were disconnected. + # Save request context as a request to send. + # We will re-try to send it later once reconnected. + self._register_to_send(request_context) + # Transfer exception to returned future. + future.set_exception(exc) + else: + write_future.add_done_callback(on_message_written) + + # 1) Synchronize requests just wait for connection. + # Other requests wait for reconnection (which also implies connection). + if isinstance(request, requests.Synchronize): + self.is_connecting.wait().add_done_callback(on_connected) + else: + self.is_reconnecting.wait().add_done_callback(on_connected) + + return future + + @gen.coroutine + def connect(self): + """ Effectively connect this object. """ + LOGGER.info('Trying to connect.') + yield self._connect() + + def _on_socket_message(self, socket_message): + """ Manage given socket_message (string), + that may be a string representation of either a request or a notification. + """ + + # Check response format and run callback (if defined). + try: + json_message = json.loads(socket_message) + except ValueError: + LOGGER.exception('Unable to parse JSON from a socket message.') + return + + if not isinstance(json_message, dict): + LOGGER.error("Unable to convert a JSON string to a dictionary.") + return + request_id = json_message.get(strings.REQUEST_ID, None) + notification_id = json_message.get(strings.NOTIFICATION_ID, None) + + if request_id: + if request_id not in self.requests_waiting_responses: + LOGGER.error('Unknown request.') + return + request_context = self.requests_waiting_responses.pop(request_id) # type: RequestFutureContext + try: + response = responses.parse_dict(json_message) + managed_data = handle_response(request_context, response) + request_context.future.set_result(managed_data) + except exceptions.ResponseException as ex: + LOGGER.error('Error received for request %s', request_context.request.name) + request_context.future.set_exception(ex) + + elif notification_id: + notification = notifications.parse_dict(json_message) + if notification.token not in self.channels: + LOGGER.error('Unknown notification: %s', notification.name) + return + notification_managers.handle_notification(self, notification) + else: + LOGGER.error('Unknown socket message.') + + @gen.coroutine + def _handle_socket_messages(self): + """ Main looping method used to received connection messages. """ + while True: + msg = yield self.connection.read_message() + if msg is None: + # Reconnect. + LOGGER.error('Disconnected.') + yield self._reconnect() + else: + # Check response format and run callback (if defined). + self._on_socket_message(msg) + + # Public methods. + + @gen.coroutine + def authenticate(self, username, password, create_user=False): + """ Send a SignIn request. + :param username: username + :param password: password + :param create_user: boolean indicating if you want to create a user or login to and existing user. + :return: a Channel object representing the authentication. + """ + request = requests.SignIn(username=username, password=password, create_user=create_user) + return (yield self.send(request)) + + def send(self, request, for_game=None): + """ Send a request. + :param request: request object. + :param for_game: (optional) NetworkGame object (required for game requests). + :return: a Future that returns the response handler result of this request. + """ + request_future = Future() + request_context = RequestFutureContext(request=request, future=request_future, connection=self, game=for_game) + + self.write_request(request_context).add_done_callback(MessageWrittenCallback(request_context).callback) + return gen.with_timeout(timedelta(seconds=constants.REQUEST_TIMEOUT_SECONDS), request_future) + +@gen.coroutine +def connect(hostname, port): + """ Connect to given hostname and port. + :param hostname: a hostname + :param port: a port + :return: a Connection object connected. + :rtype: Connection + """ + connection = Connection(hostname, port) + yield connection.connect() + return connection diff --git a/diplomacy/client/game_instances_set.py b/diplomacy/client/game_instances_set.py new file mode 100644 index 0000000..f8c22b9 --- /dev/null +++ b/diplomacy/client/game_instances_set.py @@ -0,0 +1,81 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Set of games instances (NetworkGame objects) for a same game ID for 1 channel. + Contains at most 1 game instance per map power + 1 "special" game which is either + an observer game or an omniscient game. A game instance set cannot contain both + an observer game and an omniscient game because 1 channel (ie. 1 user connected + with 1 token) can be either an observer or an omniscient, + but not both at same time. +""" +import weakref + +from diplomacy.engine.game import Game +from diplomacy.utils import exceptions + +class GameInstancesSet(): + """ Game Instances Set class. """ + __slots__ = ['game_id', 'games', 'current_observer_type'] + + def __init__(self, game_id): + """ Initialize a game instances set. + :param game_id: game ID of game instances to store. + """ + self.game_id = game_id + self.games = weakref.WeakValueDictionary() # {power name => NetworkGame} + self.current_observer_type = None + + def get_games(self): + """ Return a sequence of stored game instances. """ + return self.games.values() + + def get(self, power_name): + """ Return game instance associated to given power name. """ + return self.games.get(power_name, None) + + def get_special(self): + """ Return stored special game, or None if no special game found. """ + return self.games.get(self.current_observer_type, None) if self.current_observer_type else None + + def remove(self, role): + """ Remove game instance associated to given game role. """ + return self.games.pop(role, None) + + def remove_special(self): + """ Remove special gme. """ + self.games.pop(self.current_observer_type, None) + + def add(self, game): + """ Add given game. + :param game: a NetworkGame object. + :type game: diplomacy.client.network_game.NetworkGame + """ + assert self.game_id == game.game_id + if Game.is_player_game(game): + if game.role in self.games: + raise exceptions.DiplomacyException('Power name %s already in game instances set.' % game.role) + elif Game.is_observer_game(game): + if self.current_observer_type is not None: + raise exceptions.DiplomacyException('Previous special game %s must be removed before adding new one.' + % self.current_observer_type) + self.current_observer_type = game.role + else: + assert Game.is_omniscient_game(game) + if self.current_observer_type is not None: + raise exceptions.DiplomacyException('Previous special game %s must be removed before adding new one.' + % self.current_observer_type) + self.current_observer_type = game.role + self.games[game.role] = game diff --git a/diplomacy/client/network_game.py b/diplomacy/client/network_game.py new file mode 100644 index 0000000..e75687f --- /dev/null +++ b/diplomacy/client/network_game.py @@ -0,0 +1,199 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Game object used on client side. """ +import logging + +from diplomacy.client.channel import Channel +from diplomacy.communication import notifications +from diplomacy.engine.game import Game +from diplomacy.utils.exceptions import DiplomacyException +from diplomacy.utils.game_phase_data import GamePhaseData + +LOGGER = logging.getLogger(__name__) + +def game_request_method(channel_method): + """Create a game request method that calls channel counterpart.""" + + def func(self, **kwargs): + """ Call channel-related method to send a game request with given kwargs. """ + # NB: Channel method returns a future. + if not self.channel: + raise DiplomacyException('Invalid client game.') + return channel_method(self.channel, game_object=self, **kwargs) + + return func + +def callback_setting_method(notification_class): + """ Create a callback setting method for a given notification class. """ + + def func(self, notification_callback): + """ Add given callback for this game notification class. """ + self.add_notification_callback(notification_class, notification_callback) + + return func + +def callback_clearing_method(notification_class): + """ Create a callback clearing method for a given notification class. """ + + def func(self): + """ Clear user callbacks for this game notification class. """ + self.clear_notification_callbacks(notification_class) + + return func + +class NetworkGame(Game): + """ NetworkGame class. Properties: + - channel: associated Channel object. + - notification_callbacks: dict mapping a notification class name to a callback to be called + when a corresponding game notification is received. + """ + __slots__ = ['channel', 'notification_callbacks', 'data', '__weakref__'] + + def __init__(self, channel, received_game): + """ Initialize network game object with a channel and a game object sent by server. + :param channel: a Channel object. + :param received_game: a Game object. + :type channel: diplomacy.client.channel.Channel + :type received_game: diplomacy.Game + """ + self.channel = channel + self.notification_callbacks = {} # {notification_class => [callback(game, notification)]} + self.data = None + # Initialize parent class with Jsonable attributes from received game. + # Received game should contain a valid `initial_state` attribute that will be used + # to set client game state. + super(NetworkGame, self).__init__(**{key: getattr(received_game, key) for key in received_game.get_model()}) + + # =========== + # Public API. + # =========== + + # NB: Method get_all_possible_orders() is only local in Python code, + # but is still a network call from web interface. + get_phase_history = game_request_method(Channel.get_phase_history) + leave = game_request_method(Channel.leave_game) + send_game_message = game_request_method(Channel.send_game_message) + set_orders = game_request_method(Channel.set_orders) + + clear_centers = game_request_method(Channel.clear_centers) + clear_orders = game_request_method(Channel.clear_orders) + clear_units = game_request_method(Channel.clear_units) + + wait = game_request_method(Channel.wait) + no_wait = game_request_method(Channel.no_wait) + vote = game_request_method(Channel.vote) + save = game_request_method(Channel.save) + + def synchronize(self): + """ Send a Synchronize request to synchronize this game with associated server game. """ + if not self.channel: + raise DiplomacyException('Invalid client game.') + return self.channel.synchronize(game_object=self, timestamp=self.get_latest_timestamp()) + + # Admin / Moderator API. + delete = game_request_method(Channel.delete_game) + kick_powers = game_request_method(Channel.kick_powers) + set_state = game_request_method(Channel.set_state) + process = game_request_method(Channel.process) + query_schedule = game_request_method(Channel.query_schedule) + start = game_request_method(Channel.start) + pause = game_request_method(Channel.pause) + resume = game_request_method(Channel.resume) + cancel = game_request_method(Channel.cancel) + draw = game_request_method(Channel.draw) + + # =============================== + # Notification callback settings. + # =============================== + + add_on_cleared_centers = callback_setting_method(notifications.ClearedCenters) + add_on_cleared_orders = callback_setting_method(notifications.ClearedOrders) + add_on_cleared_units = callback_setting_method(notifications.ClearedUnits) + add_on_game_deleted = callback_setting_method(notifications.GameDeleted) + add_on_game_message_received = callback_setting_method(notifications.GameMessageReceived) + add_on_game_processed = callback_setting_method(notifications.GameProcessed) + add_on_game_phase_update = callback_setting_method(notifications.GamePhaseUpdate) + add_on_game_status_update = callback_setting_method(notifications.GameStatusUpdate) + add_on_omniscient_updated = callback_setting_method(notifications.OmniscientUpdated) + add_on_power_orders_flag = callback_setting_method(notifications.PowerOrdersFlag) + add_on_power_orders_update = callback_setting_method(notifications.PowerOrdersUpdate) + add_on_power_vote_updated = callback_setting_method(notifications.PowerVoteUpdated) + add_on_power_wait_flag = callback_setting_method(notifications.PowerWaitFlag) + add_on_powers_controllers = callback_setting_method(notifications.PowersControllers) + add_on_vote_count_updated = callback_setting_method(notifications.VoteCountUpdated) + add_on_vote_updated = callback_setting_method(notifications.VoteUpdated) + + clear_on_cleared_centers = callback_clearing_method(notifications.ClearedCenters) + clear_on_cleared_orders = callback_clearing_method(notifications.ClearedOrders) + clear_on_cleared_units = callback_clearing_method(notifications.ClearedUnits) + clear_on_game_deleted = callback_clearing_method(notifications.GameDeleted) + clear_on_game_message_received = callback_clearing_method(notifications.GameMessageReceived) + clear_on_game_processed = callback_clearing_method(notifications.GameProcessed) + clear_on_game_phase_update = callback_clearing_method(notifications.GamePhaseUpdate) + clear_on_game_status_update = callback_clearing_method(notifications.GameStatusUpdate) + clear_on_omniscient_updated = callback_clearing_method(notifications.OmniscientUpdated) + clear_on_power_orders_flag = callback_clearing_method(notifications.PowerOrdersFlag) + clear_on_power_orders_update = callback_clearing_method(notifications.PowerOrdersUpdate) + clear_on_power_vote_updated = callback_clearing_method(notifications.PowerVoteUpdated) + clear_on_power_wait_flag = callback_clearing_method(notifications.PowerWaitFlag) + clear_on_powers_controllers = callback_clearing_method(notifications.PowersControllers) + clear_on_vote_count_updated = callback_clearing_method(notifications.VoteCountUpdated) + clear_on_vote_updated = callback_clearing_method(notifications.VoteUpdated) + + def add_notification_callback(self, notification_class, notification_callback): + """ Add a callback for a notification. + :param notification_class: a notification class + :param notification_callback: callback to add. + """ + assert callable(notification_callback) + if notification_class not in self.notification_callbacks: + self.notification_callbacks[notification_class] = [notification_callback] + else: + self.notification_callbacks[notification_class].append(notification_callback) + + def clear_notification_callbacks(self, notification_class): + """ Remove all user callbacks for a notification. + :param notification_class: a notification class + """ + self.notification_callbacks.pop(notification_class, None) + + def notify(self, notification): + """ Notify game with given notification (call associated callbacks if defined). """ + for callback in self.notification_callbacks.get(type(notification), ()): + callback(self, notification) + + def set_phase_data(self, phase_data, clear_history=True): + """ Overwrite base method to prevent call to channel methods. """ + if not phase_data: + return + if isinstance(phase_data, GamePhaseData): + phase_data = [phase_data] + elif not isinstance(phase_data, list): + phase_data = list(phase_data) + + if clear_history: + self._clear_history() + + for game_phase_data in phase_data[:-1]: # type: GamePhaseData + Game.extend_phase_history(self, game_phase_data) + + current_phase_data = phase_data[-1] # type: GamePhaseData + Game.set_state(self, current_phase_data.state, clear_history=clear_history) + for power_name, power_orders in current_phase_data.orders.items(): + Game.set_orders(self, power_name, power_orders) + self.messages = current_phase_data.messages.copy() + # We ignore 'results' for current phase data. diff --git a/diplomacy/client/notification_managers.py b/diplomacy/client/notification_managers.py new file mode 100644 index 0000000..52b0b14 --- /dev/null +++ b/diplomacy/client/notification_managers.py @@ -0,0 +1,266 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Notification managers (client side). """ +# pylint: disable=unused-argument +import logging + +from diplomacy.client.network_game import NetworkGame +from diplomacy.communication import notifications +from diplomacy.engine.game import Game +from diplomacy.utils import exceptions, strings + +LOGGER = logging.getLogger(__name__) + +def _get_game_to_notify(connection, notification): + """ Get notified game from connection using notification parameters. + :param connection: connection that receives the notification. + :param notification: notification received. + :return: a NetWorkGame instance, or None if no game found. + :type connection: diplomacy.Connection + :type notification: diplomacy.communication.notifications._GameNotification + """ + channel = connection.channels.get(notification.token, None) + if channel and notification.game_id in channel.game_id_to_instances: + return channel.game_id_to_instances[notification.game_id].get(notification.game_role) + return None + +def on_account_deleted(channel, notification): + """ Manage notification AccountDeleted. + :param channel: channel associated to received notification. + :param notification: received notification. + :type channel: diplomacy.client.channel.Channel + """ + # We remove channel from related connection. + channel.connection.channels.pop(channel.token) + +def on_cleared_centers(game, notification): + """ Manage notification ClearedCenters. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.ClearedCenters + """ + Game.clear_centers(game, notification.power_name) + +def on_cleared_orders(game, notification): + """ Manage notification ClearedOrders. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.ClearedOrders + """ + Game.clear_orders(game, notification.power_name) + +def on_cleared_units(game, notification): + """ Manage notification ClearedUnits. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.ClearedUnits + """ + Game.clear_units(game, notification.power_name) + +def on_powers_controllers(game, notification): + """ Manage notification PowersControllers. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.PowersControllers + """ + if Game.is_player_game(game) and notification.powers[game.power.name] != game.power.get_controller(): + # Player is now invalid. We just remove game from related channel. + game.channel.game_id_to_instances[game.game_id].remove(game.power.name) + else: + # In any other case, update powers controllers. + Game.update_powers_controllers(game, notification.powers, notification.timestamps) + +def on_game_deleted(game, notification): + """ Manage notification GameDeleted. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + """ + # We remove game from related channel. + if Game.is_player_game(game): + game.channel.game_id_to_instances[game.game_id].remove(game.power.name) + else: + game.channel.game_id_to_instances[game.game_id].remove_special() + +def on_game_message_received(game, notification): + """ Manage notification GameMessageReceived.. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.GameMessageReceived + """ + Game.add_message(game, notification.message) + +def on_game_processed(game, notification): + """ Manage notification GamePhaseUpdate (for omniscient and observer games). + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.GameProcessed + """ + game.set_phase_data([notification.previous_phase_data, notification.current_phase_data], clear_history=False) + +def on_game_phase_update(game, notification): + """ Manage notification GamePhaseUpdate. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.GamePhaseUpdate + """ + if notification.phase_data_type == strings.STATE_HISTORY: + Game.extend_phase_history(game, notification.phase_data) + else: + game.set_phase_data(notification.phase_data) + +def on_game_status_update(game, notification): + """ Manage notification GameStatusUpdate. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.GameStatusUpdate + """ + Game.set_status(game, notification.status) + +def on_omniscient_updated(game, notification): + """ Manage notification OmniscientUpdated. + :param game: game associated to received notification. + :param notification: received notification. + :type game: diplomacy.client.network_game.NetworkGame + :type notification: notifications.OmniscientUpdated + """ + assert not Game.is_player_game(game) + if Game.is_observer_game(game): + assert notification.grade_update == strings.PROMOTE + assert notification.game.is_omniscient_game() + else: + assert notification.grade_update == strings.DEMOTE + assert notification.game.is_observer_game() + # Save client game channel and invalidate client game. + channel = game.channel + game.channel = None + channel.game_id_to_instances[notification.game_id].remove(game.role) + # Create a new client game with previous client game channel game sent by server. + new_game = NetworkGame(channel, notification.game) + new_game.notification_callbacks.update({key: value.copy() for key, value in game.notification_callbacks.items()}) + new_game.data = game.data + channel.game_id_to_instances[notification.game_id].add(new_game) + +def on_power_orders_update(game, notification): + """ Manage notification PowerOrdersUpdate. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.PowerOrdersUpdate + """ + Game.set_orders(game, notification.power_name, notification.orders) + +def on_power_orders_flag(game, notification): + """ Manage notification PowerOrdersFlag. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.PowerOrdersFlag + """ + # A power should not receive an order flag notification for itself. + assert game.is_player_game() and game.power.name != notification.power_name + game.get_power(notification.power_name).order_is_set = notification.order_is_set + +def on_power_vote_updated(game, notification): + """ Manage notification PowerVoteUpdated (for power game). + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.PowerVoteUpdated + """ + assert Game.is_player_game(game) + game.power.vote = notification.vote + +def on_power_wait_flag(game, notification): + """ Manage notification PowerWaitFlag. + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.PowerWaitFlag + """ + Game.set_wait(game, notification.power_name, notification.wait) + +def on_vote_count_updated(game, notification): + """ Manage notification VoteCountUpdated (for observer game). + :param game: game associated to received notification. + :param notification: received notification. + :type game: diplomacy.client.network_game.NetworkGame + """ + assert Game.is_observer_game(game) + +def on_vote_updated(game, notification): + """ Manage notification VoteUpdated (for omniscient game). + :param game: a Network game + :param notification: notification received + :type game: diplomacy.client.network_game.NetworkGame + :type notification: diplomacy.communication.notifications.VoteUpdated + """ + assert Game.is_omniscient_game(game) + for power_name, vote in notification.vote.items(): + Game.get_power(game, power_name).vote = vote + +# Mapping dictionary from notification class to notification handler function. +MAPPING = { + notifications.AccountDeleted: on_account_deleted, + notifications.ClearedCenters: on_cleared_centers, + notifications.ClearedOrders: on_cleared_orders, + notifications.ClearedUnits: on_cleared_units, + notifications.GameDeleted: on_game_deleted, + notifications.GameMessageReceived: on_game_message_received, + notifications.GameProcessed: on_game_processed, + notifications.GamePhaseUpdate: on_game_phase_update, + notifications.GameStatusUpdate: on_game_status_update, + notifications.OmniscientUpdated: on_omniscient_updated, + notifications.PowerOrdersFlag: on_power_orders_flag, + notifications.PowerOrdersUpdate: on_power_orders_update, + notifications.PowersControllers: on_powers_controllers, + notifications.PowerVoteUpdated: on_power_vote_updated, + notifications.PowerWaitFlag: on_power_wait_flag, + notifications.VoteCountUpdated: on_vote_count_updated, + notifications.VoteUpdated: on_vote_updated, +} + +def handle_notification(connection, notification): + """ Call appropriate handler for given notification received by given connection. + :param connection: recipient connection. + :param notification: received notification. + :type connection: diplomacy.Connection + :type notification: notifications._AbstractNotification | notifications._GameNotification + """ + if notification.level == strings.CHANNEL: + object_to_notify = connection.channels.get(notification.token, None) + else: + object_to_notify = _get_game_to_notify(connection, notification) + if object_to_notify is None: + LOGGER.error('Unknown notification: %s', notification.name) + else: + LOGGER.info('Notification received: %s', notification.name) + handler = MAPPING.get(type(notification), None) + if not handler: + raise exceptions.DiplomacyException( + 'No handler available for notification class %s' % type(notification).__name__) + handler(object_to_notify, notification) + if notification.level == strings.GAME: + object_to_notify.notify(notification) diff --git a/diplomacy/client/response_managers.py b/diplomacy/client/response_managers.py new file mode 100644 index 0000000..5183ac4 --- /dev/null +++ b/diplomacy/client/response_managers.py @@ -0,0 +1,335 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Response managers (client side). One manager corresponds to one request, except for requests that don't need + specific manager (in such case, method default_manager() is used). + Each manager is a function with name format "on_<request name in snake case>", expecting a request context + and a response as parameters. +""" +# pylint: disable=unused-argument +from diplomacy.client.game_instances_set import GameInstancesSet +from diplomacy.client.network_game import NetworkGame +from diplomacy.client.channel import Channel +from diplomacy.communication import requests, responses +from diplomacy.engine.game import Game +from diplomacy.utils import exceptions +from diplomacy.utils.game_phase_data import GamePhaseData + +class RequestFutureContext(): + """ Helper class to store a context around a request + (with future for response management, related connection and optional related game). + """ + __slots__ = ['request', 'future', 'connection', 'game'] + + def __init__(self, request, future, connection, game=None): + """ Initialize a request future context. + :param request: a request object (see diplomacy.communication.requests about possible classes). + :param future: a tornado Future object. + :param connection: a diplomacy.Connection object. + :param game: (optional) a NetworkGame object (from module diplomacy.client.network_game). + :type request: requests._AbstractRequest | requests._AbstractGameRequest + :type future: tornado.concurrent.Future + :type connection: diplomacy.Connection + :type game: diplomacy.client.network_game.NetworkGame + """ + self.request = request + self.future = future + self.connection = connection + self.game = game + + request_id = property(lambda self: self.request.request_id) + token = property(lambda self: self.request.token) + channel = property(lambda self: self.connection.channels[self.request.token]) + + def new_channel(self, token): + """ Create, store (in associated connection), and return a new channel with given token. """ + channel = Channel(self.connection, token) + self.connection.channels[token] = channel + return channel + + def new_game(self, received_game): + """ Create, store (in associated connection) and return a new network game wrapping given game data. + Returned game is already in appropriate type (observer game, omniscient game or power game). + :param received_game: game sent by server (Game object) + :type received_game: Game + """ + game = NetworkGame(self.channel, received_game) + if game.game_id not in self.channel.game_id_to_instances: + self.channel.game_id_to_instances[game.game_id] = GameInstancesSet(game.game_id) + self.channel.game_id_to_instances[game.game_id].add(game) + return game + + def remove_channel(self): + """ Remove associated channel (inferred from request token) from associated connection. """ + del self.connection.channels[self.channel.token] + + def delete_game(self): + """ Delete local game instances corresponding to game ID in associated request. """ + assert hasattr(self.request, 'game_id') + assert self.game is not None and self.game.game_id == self.request.game_id + if self.request.game_id in self.channel.game_id_to_instances: + del self.channel.game_id_to_instances[self.request.game_id] + +def default_manager(context, response): + """ Default manager called for requests that don't have specific management. + If response is OK, return None. + If response is a UniqueData, return response data field. + Else, return response. + Expect response to be either OK or a UniqueData + (containing only 1 field intended to be returned by server for associated request). + :param context: request context + :param response: response received + :return: None, or data if response is a UniqueData. + """ + if isinstance(response, responses.UniqueData): + return response.data + if isinstance(response, responses.Ok): + return None + return response + +def on_create_game(context, response): + """ Manage response for request CreateGame. + :param context: request context + :param response: response received + :return: a new network game + :type context: RequestFutureContext + :type response: responses.DataGame + """ + return context.new_game(response.data) + +def on_delete_account(context, response): + """ Manage response for request DeleteAccount. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + context.remove_channel() + +def on_delete_game(context, response): + """ Manage response for request DeleteGame. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + context.delete_game() + +def on_get_phase_history(context, response): + """ Manage response for request GetPhaseHistory. + :param context: request context + :param response: response received + :return: a list of game states + :type context: RequestFutureContext + :type response: responses.DataGamePhases + """ + phase_history = response.data + for game_phase in phase_history: # type: diplomacy.utils.game_phase_data.GamePhaseData + Game.extend_phase_history(context.game, game_phase) + return phase_history + +def on_join_game(context, response): + """ Manage response for request JoinGame. + :param context: request context + :param response: response received + :return: a new network game + :type response: responses.DataGame + """ + return context.new_game(response.data) + +def on_leave_game(context, response): + """ Manage response for request LeaveGame. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + context.delete_game() + +def on_logout(context, response): + """ Manage response for request Logout. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + context.remove_channel() + +def on_send_game_message(context, response): + """ Manage response for request SendGameMessage. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + :type response: responses.DataTimeStamp + """ + request = context.request # type: requests.SendGameMessage + message = request.message + message.time_sent = response.data + Game.add_message(context.game, message) + +def on_set_game_state(context, response): + """ Manage response for request SetGameState. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.SetGameState + context.game.set_phase_data(GamePhaseData(name=request.state['name'], + state=request.state, + orders=request.orders, + messages=request.messages, + results=request.results)) + +def on_set_game_status(context, response): + """ Manage response for request SetGameStatus. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.SetGameStatus + Game.set_status(context.game, request.status) + +def on_set_orders(context, response): + """ Manage response for request SetOrders. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.SetOrders + orders = request.orders + if Game.is_player_game(context.game): + assert context.game.power.name == context.request.game_role + Game.set_orders(context.game, request.game_role, orders) + else: + Game.set_orders(context.game, request.power_name, orders) + +def on_clear_orders(context, response): + """ Manage response for request ClearOrders. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.ClearOrders + Game.clear_orders(context.game, request.power_name) + +def on_clear_centers(context, response): + """ Manage response for request ClearCenters. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.ClearCenters + Game.clear_centers(context.game, request.power_name) + +def on_clear_units(context, response): + """ Manage response for request ClearUnits. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.ClearUnits + Game.clear_units(context.game, request.power_name) + +def on_set_wait_flag(context, response): + """ Manage response for request SetWaitFlag. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.SetWaitFlag + wait = request.wait + if Game.is_player_game(context.game): + assert context.game.power.name == context.request.game_role + Game.set_wait(context.game, request.game_role, wait) + else: + Game.set_wait(context.game, request.power_name, wait) + +def on_sign_in(context, response): + """ Manage response for request SignIn. + :param context: request context + :param response: response received + :return: a new channel + :type context: RequestFutureContext + :type response: responses.DataToken + """ + return context.new_channel(response.data) + +def on_vote(context, response): + """ Manage response for request VoteAboutDraw. + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.Vote + vote = request.vote + assert Game.is_player_game(context.game) + assert context.game.power.name == context.request.game_role + context.game.power.vote = vote + +# Mapping dictionary from request class to response handler function. +MAPPING = { + requests.ClearCenters: on_clear_centers, + requests.ClearOrders: on_clear_orders, + requests.ClearUnits: on_clear_units, + requests.CreateGame: on_create_game, + requests.DeleteAccount: on_delete_account, + requests.DeleteGame: on_delete_game, + requests.GetAllPossibleOrders: default_manager, + requests.GetAvailableMaps: default_manager, + requests.GetDummyWaitingPowers: default_manager, + requests.GetPlayablePowers: default_manager, + requests.GetPhaseHistory: on_get_phase_history, + requests.JoinGame: on_join_game, + requests.JoinPowers: default_manager, + requests.LeaveGame: on_leave_game, + requests.ListGames: default_manager, + requests.GetGamesInfo: default_manager, + requests.Logout: on_logout, + requests.ProcessGame: default_manager, + requests.QuerySchedule: default_manager, + requests.SaveGame: default_manager, + requests.SendGameMessage: on_send_game_message, + requests.SetDummyPowers: default_manager, + requests.SetGameState: on_set_game_state, + requests.SetGameStatus: on_set_game_status, + requests.SetGrade: default_manager, + requests.SetOrders: on_set_orders, + requests.SetWaitFlag: on_set_wait_flag, + requests.SignIn: on_sign_in, + requests.Synchronize: default_manager, + requests.Vote: on_vote, +} + +def handle_response(context, response): + """ Call appropriate handler for given response with given request context. + :param context: request context. + :param response: response received. + :return: value returned by handler. + """ + handler = MAPPING.get(type(context.request), None) + if not handler: + raise exceptions.DiplomacyException( + 'No response handler available for request class %s' % type(context.request).__name__) + return handler(context, response) diff --git a/diplomacy/communication/__init__.py b/diplomacy/communication/__init__.py new file mode 100644 index 0000000..acc0ee4 --- /dev/null +++ b/diplomacy/communication/__init__.py @@ -0,0 +1,16 @@ +# ============================================================================== +# 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/>. +# ============================================================================== diff --git a/diplomacy/communication/notifications.py b/diplomacy/communication/notifications.py new file mode 100644 index 0000000..c88d526 --- /dev/null +++ b/diplomacy/communication/notifications.py @@ -0,0 +1,255 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +"""Server -> Client notifications.""" +import inspect + +from diplomacy.engine.game import Game +from diplomacy.engine.message import Message +from diplomacy.utils import common, exceptions, parsing, strings +from diplomacy.utils.network_data import NetworkData +from diplomacy.utils.constants import OrderSettings +from diplomacy.utils.game_phase_data import GamePhaseData + +class _AbstractNotification(NetworkData): + """ Base notification object """ + __slots__ = ['notification_id', 'token'] + header = { + strings.NOTIFICATION_ID: str, + strings.NAME: str, + strings.TOKEN: str, + } + params = {} + id_field = strings.NOTIFICATION_ID + level = None + + def __init__(self, **kwargs): + self.notification_id = None # type: str + self.token = None # type: str + super(_AbstractNotification, self).__init__(**kwargs) + + @classmethod + def validate_params(cls): + """ Hack: we just use it to validate level. """ + assert cls.level in strings.ALL_COMM_LEVELS + +class _ChannelNotification(_AbstractNotification): + """ Channel notification (intended to be sent to a channel). """ + __slots__ = [] + level = strings.CHANNEL + +class _GameNotification(_AbstractNotification): + """ Game notification (intended to be sent to a game). """ + __slots__ = ['game_id', 'game_role', 'power_name'] + header = parsing.update_model(_AbstractNotification.header, { + strings.GAME_ID: str, + strings.GAME_ROLE: str, + strings.POWER_NAME: parsing.OptionalValueType(str), + }) + + level = strings.GAME + + def __init__(self, **kwargs): + self.game_id = None # type: str + self.game_role = None # type: str + self.power_name = None # type: str + super(_GameNotification, self).__init__(**kwargs) + +class AccountDeleted(_ChannelNotification): + """ Notification about an account deleted. """ + __slots__ = [] + +class OmniscientUpdated(_GameNotification): + """ Notification about a grade updated. Sent at channel level. """ + __slots__ = ['grade_update', 'game'] + params = { + strings.GRADE_UPDATE: parsing.EnumerationType(strings.ALL_GRADE_UPDATES), + strings.GAME: parsing.JsonableClassType(Game) + } + + def __init__(self, **kwargs): + self.grade_update = '' + self.game = None # type: Game + super(OmniscientUpdated, self).__init__(**kwargs) + +class ClearedCenters(_GameNotification): + """ Notification about centers cleared. """ + __slots__ = [] + +class ClearedOrders(_GameNotification): + """ Notification about orders cleared. """ + __slots__ = [] + +class ClearedUnits(_GameNotification): + """ Notification about units cleared. """ + __slots__ = [] + +class VoteCountUpdated(_GameNotification): + """ Notification about new count of draw votes for a game (for observers) + Properties: + - count_voted: number of powers that have voted. + - count_expected: number of powers to be expected to vote. + """ + __slots__ = ['count_voted', 'count_expected'] + params = { + strings.COUNT_VOTED: int, + strings.COUNT_EXPECTED: int, + } + + def __init__(self, **kwargs): + self.count_voted = None # type: int + self.count_expected = None # type: int + super(VoteCountUpdated, self).__init__(**kwargs) + +class VoteUpdated(_GameNotification): + """ Notification about votes updated for a game (for omniscient observers). Properties: + - vote: dictionary mapping a power name to a Vote object representing power vote. + """ + __slots__ = ['vote'] + params = { + strings.VOTE: parsing.DictType(str, parsing.EnumerationType(strings.ALL_VOTE_DECISIONS)) + } + + def __init__(self, **kwargs): + self.vote = None # type: dict{str, str} + super(VoteUpdated, self).__init__(**kwargs) + +class PowerVoteUpdated(VoteCountUpdated): + """ Notification about a new vote for a specific game power (for player games). Properties: + - vote: vote object representing associated power vote. + """ + __slots__ = ['vote'] + params = parsing.extend_model(VoteCountUpdated.params, { + strings.VOTE: parsing.EnumerationType(strings.ALL_VOTE_DECISIONS) + }) + + def __init__(self, **kwargs): + self.vote = None # type: str + super(PowerVoteUpdated, self).__init__(**kwargs) + +class PowersControllers(_GameNotification): + """ Notification about current controller for each power in a game. """ + __slots__ = ['powers', 'timestamps'] + params = { + # {power_name => controller_name} + strings.POWERS: parsing.DictType(str, parsing.OptionalValueType(str)), + # {power_name => controller timestamp} + strings.TIMESTAMPS: parsing.DictType(str, int) + } + + def __init__(self, **kwargs): + self.powers = {} + self.timestamps = {} + super(PowersControllers, self).__init__(**kwargs) + +class GameDeleted(_GameNotification): + """ Notification about a game deleted. """ + __slots__ = [] + +class GameProcessed(_GameNotification): + """ Notification about a game phase update. Sent after game had processed a phase. """ + __slots__ = ['previous_phase_data', 'current_phase_data'] + params = { + strings.PREVIOUS_PHASE_DATA: parsing.JsonableClassType(GamePhaseData), + strings.CURRENT_PHASE_DATA: parsing.JsonableClassType(GamePhaseData), + } + + def __init__(self, **kwargs): + self.previous_phase_data = None # type: GamePhaseData + self.current_phase_data = None # type: GamePhaseData + super(GameProcessed, self).__init__(**kwargs) + +class GamePhaseUpdate(_GameNotification): + """ Notification about a game phase update. """ + __slots__ = ['phase_data', 'phase_data_type'] + params = { + strings.PHASE_DATA: parsing.JsonableClassType(GamePhaseData), + strings.PHASE_DATA_TYPE: strings.ALL_STATE_TYPES + } + + def __init__(self, **kwargs): + self.phase_data = None # type: GamePhaseData + self.phase_data_type = None # type: str + super(GamePhaseUpdate, self).__init__(**kwargs) + +class GameStatusUpdate(_GameNotification): + """ Notification about a game status update. """ + __slots__ = ['status'] + params = { + strings.STATUS: parsing.EnumerationType(strings.ALL_GAME_STATUSES), + } + + def __init__(self, **kwargs): + self.status = None + super(GameStatusUpdate, self).__init__(**kwargs) + +class GameMessageReceived(_GameNotification): + """ Notification about a game message received. """ + __slots__ = ['message'] + params = { + strings.MESSAGE: parsing.JsonableClassType(Message), + } + + def __init__(self, **kwargs): + self.message = None # type: Message + super(GameMessageReceived, self).__init__(**kwargs) + +class PowerOrdersUpdate(_GameNotification): + """ Notification about a power order update. """ + __slots__ = ['orders'] + params = { + strings.ORDERS: parsing.OptionalValueType(parsing.SequenceType(str)), + } + + def __init__(self, **kwargs): + self.orders = None # type: set + super(PowerOrdersUpdate, self).__init__(**kwargs) + +class PowerOrdersFlag(_GameNotification): + """ Notification about a power order flag update. """ + __slots__ = ['order_is_set'] + params = { + strings.ORDER_IS_SET: parsing.EnumerationType(OrderSettings.ALL_SETTINGS), + } + + def __init__(self, **kwargs): + self.order_is_set = 0 + super(PowerOrdersFlag, self).__init__(**kwargs) + +class PowerWaitFlag(_GameNotification): + """ Notification about a power wait flag update. """ + __slots__ = ['wait'] + params = { + strings.WAIT: bool, + } + + def __init__(self, **kwargs): + self.wait = None # type: bool + super(PowerWaitFlag, self).__init__(**kwargs) + +def parse_dict(json_notification): + """ Parse a JSON expected to represent a notification. Raise an exception if parsing failed. + :param json_notification: JSON dictionary. + :return: a notification class instance. + """ + assert isinstance(json_notification, dict), 'Notification parser expects a dict.' + name = json_notification.get(strings.NAME, None) + if name is None: + raise exceptions.NotificationException() + expected_class_name = common.snake_case_to_upper_camel_case(name) + notification_class = globals()[expected_class_name] + assert inspect.isclass(notification_class) and issubclass(notification_class, _AbstractNotification) + return notification_class.from_dict(json_notification) diff --git a/diplomacy/communication/requests.py b/diplomacy/communication/requests.py new file mode 100644 index 0000000..b7f7671 --- /dev/null +++ b/diplomacy/communication/requests.py @@ -0,0 +1,572 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Client -> Server requests. + Notes: + If an error occurred on server-side while handling a request, client will receive + a ResponseException containing message about handling error. Request exceptions + are currently not more typed on client-side. +""" +import inspect +import logging + +from diplomacy.engine.message import Message +from diplomacy.utils import common, exceptions, parsing, strings +from diplomacy.utils.network_data import NetworkData +from diplomacy.utils.parsing import OptionalValueType +from diplomacy.utils.sorted_dict import SortedDict + +LOGGER = logging.getLogger(__name__) + +class _AbstractRequest(NetworkData): + """ Abstract request class. + + Field request_id is auto-filled if not defined. + + Field name is auto-filled with snake case version of request class name. + + Field re_sent is False by default. It should be set to True if request is re-sent by client + (currently done by Connection object when reconnecting). + + Timestamp field is auto-set with current local timestamp if not defined. + For game request Synchronize, timestamp should be game latest timestamp instead + (see method NetworkGame.synchronize()). + """ + + __slots__ = ['request_id', 're_sent'] + header = { + strings.REQUEST_ID: str, + strings.NAME: str, + strings.RE_SENT: parsing.DefaultValueType(bool, False), + } + params = {} + id_field = strings.REQUEST_ID + level = None + + def __init__(self, **kwargs): + self.request_id = None # type: str + self.re_sent = None # type: bool + super(_AbstractRequest, self).__init__(**kwargs) + + @classmethod + def validate_params(cls): + """ Hack: we just use it to validate level. """ + assert cls.level is None or cls.level in strings.ALL_COMM_LEVELS + +class _AbstractChannelRequest(_AbstractRequest): + """ Abstract class representing a channel request. + Token field is automatically filled by a Channel object before sending request. + """ + + __slots__ = ['token'] + header = parsing.update_model(_AbstractRequest.header, { + strings.TOKEN: str + }) + level = strings.CHANNEL + + def __init__(self, **kwargs): + self.token = None # type: str + super(_AbstractChannelRequest, self).__init__(**kwargs) + +class _AbstractGameRequest(_AbstractChannelRequest): + """ Abstract class representing a game request. + Game ID, game role and phase fields are automatically filled by a NetworkGame object before sending request. + """ + + __slots__ = ['game_id', 'game_role', 'phase'] + + header = parsing.extend_model(_AbstractChannelRequest.header, { + strings.GAME_ID: str, + strings.GAME_ROLE: str, + strings.PHASE: str, # Game short phase. + }) + level = strings.GAME + + # Game request class flag to indicate if this type of game request depends on game phase. + # If True, phase indicated in request must match current game phase. + phase_dependent = True + + def __init__(self, **kwargs): + self.game_id = None # type: str + self.game_role = None # type: str + self.phase = None # type: str + super(_AbstractGameRequest, self).__init__(**kwargs) + + # Return "address" of request sender inside related game (ie. channel token + game role). + # Used by certain request managers to skip sender when notify related game. + # See request managers in diplomacy.server.request_managers. + address_in_game = property(lambda self: (self.game_role, self.token)) + +# ==================== +# Connection requests. +# ==================== + +class SignIn(_AbstractRequest): + """ SignIn request. + Expected response: responses.DataToken + Expected response handler result: diplomacy.client.channel.Channel + """ + __slots__ = ['username', 'password', 'create_user'] + params = { + strings.USERNAME: str, + strings.PASSWORD: str, + strings.CREATE_USER: bool + } + + def __init__(self, **kwargs): + self.username = None + self.password = None + self.create_user = None + super(SignIn, self).__init__(**kwargs) + +# ================= +# Channel requests. +# ================= + +class CreateGame(_AbstractChannelRequest): + """ CreateGame request. + Expected response: responses.DataGame + Expected response handler result: diplomacy.client.network_game.NetworkGame + """ + __slots__ = ['game_id', 'power_name', 'state', 'map_name', 'rules', 'n_controls', 'deadline', + 'registration_password'] + params = { + strings.GAME_ID: parsing.OptionalValueType(str), + strings.N_CONTROLS: parsing.OptionalValueType(int), + strings.DEADLINE: parsing.DefaultValueType(int, 300), # 300 seconds. Must be >= 0. + strings.REGISTRATION_PASSWORD: parsing.OptionalValueType(str), + strings.POWER_NAME: parsing.OptionalValueType(str), + strings.STATE: parsing.OptionalValueType(dict), + strings.MAP_NAME: parsing.DefaultValueType(str, 'standard'), + strings.RULES: parsing.OptionalValueType(parsing.SequenceType(str, sequence_builder=set)), + } + + def __init__(self, **kwargs): + self.game_id = '' + self.n_controls = 0 + self.deadline = 0 + self.registration_password = '' + self.power_name = '' + self.state = {} + self.map_name = '' + self.rules = set() + super(CreateGame, self).__init__(**kwargs) + +class DeleteAccount(_AbstractChannelRequest): + """ DeleteAccount request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['username'] + params = { + strings.USERNAME: OptionalValueType(str) + } + + def __init__(self, **kwargs): + self.username = None + super(DeleteAccount, self).__init__(**kwargs) + +class GetDummyWaitingPowers(_AbstractChannelRequest): + """ GetDummyWaitingPowers request. + Expected response: response.DataGamesToPowerNames + Expected response handler result: {dict mapping game IDs to lists of dummy powers names} + """ + __slots__ = ['buffer_size'] + params = { + strings.BUFFER_SIZE: int, + } + + def __init__(self, **kwargs): + self.buffer_size = 0 + super(GetDummyWaitingPowers, self).__init__(**kwargs) + +class GetAvailableMaps(_AbstractChannelRequest): + """ GetAvailableMaps request. + Expected response: responses.DataMaps + Expected response handler result: {map name => [map power names]} + """ + __slots__ = [] + +class GetPlayablePowers(_AbstractChannelRequest): + """ GetPlayablePowers request. + Expected response: responses.DataPowerNames + Expected response handler result: [power names] + """ + __slots__ = ['game_id'] + params = { + strings.GAME_ID: str + } + + def __init__(self, **kwargs): + self.game_id = None + super(GetPlayablePowers, self).__init__(**kwargs) + +class JoinGame(_AbstractChannelRequest): + """ JoinGame request. + Expected response: responses.DataGame + Expected response handler result: diplomacy.client.network_game.NetworkGame + """ + __slots__ = ['game_id', 'power_name', 'registration_password'] + params = { + strings.GAME_ID: str, + strings.POWER_NAME: parsing.OptionalValueType(str), + strings.REGISTRATION_PASSWORD: parsing.OptionalValueType(str) + } + + def __init__(self, **kwargs): + self.game_id = None + self.power_name = None + self.registration_password = None + super(JoinGame, self).__init__(**kwargs) + +class JoinPowers(_AbstractChannelRequest): + """ JoinPowers request to join many powers of a game with one query. + Useful to control many powers while still working only with 1 client game instance. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['game_id', 'power_names', 'registration_password'] + params = { + strings.GAME_ID: str, + strings.POWER_NAMES: parsing.SequenceType(str, sequence_builder=set), + strings.REGISTRATION_PASSWORD: parsing.OptionalValueType(str) + } + + def __init__(self, **kwargs): + self.game_id = None + self.power_names = None + self.registration_password = None + super(JoinPowers, self).__init__(**kwargs) + +class ListGames(_AbstractChannelRequest): + """ ListGames request. + Expected response: responses.DataGames + Expected response handler result: responses.DataGames + """ + __slots__ = ['game_id', 'status', 'map_name', 'include_protected', 'for_omniscience'] + params = { + strings.STATUS: OptionalValueType(parsing.EnumerationType(strings.ALL_GAME_STATUSES)), + strings.MAP_NAME: OptionalValueType(str), + strings.INCLUDE_PROTECTED: parsing.DefaultValueType(bool, True), + strings.FOR_OMNISCIENCE: parsing.DefaultValueType(bool, False), + strings.GAME_ID: OptionalValueType(str), + } + + def __init__(self, **kwargs): + self.game_id = None + self.status = None + self.map_name = None + self.include_protected = None + self.for_omniscience = None + super(ListGames, self).__init__(**kwargs) + +class GetGamesInfo(_AbstractChannelRequest): + """ Request used to get info for a given list of game IDs. + Expected response: responses.DataGames + Expected response handler result: responses.DataGames + """ + __slots__ = ['games'] + params = { + strings.GAMES: parsing.SequenceType(str) + } + def __init__(self, **kwargs): + self.games = [] + super(GetGamesInfo, self).__init__(**kwargs) + +class Logout(_AbstractChannelRequest): + """ Logout request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = [] + +class SetGrade(_AbstractChannelRequest): + """ SetGrade request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['grade', 'grade_update', 'username', 'game_id'] + params = { + strings.GRADE: parsing.EnumerationType(strings.ALL_GRADES), + strings.GRADE_UPDATE: parsing.EnumerationType(strings.ALL_GRADE_UPDATES), + strings.USERNAME: str, + strings.GAME_ID: parsing.OptionalValueType(str), + } + + def __init__(self, **kwargs): + self.grade = None + self.grade_update = None + self.username = None + self.game_id = None + super(SetGrade, self).__init__(**kwargs) + +# ============== +# Game requests. +# ============== + +class ClearCenters(_AbstractGameRequest): + """ ClearCenters request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['power_name'] + params = { + strings.POWER_NAME: parsing.OptionalValueType(str), + } + + def __init__(self, **kwargs): + self.power_name = None # type: str + super(ClearCenters, self).__init__(**kwargs) + +class ClearOrders(_AbstractGameRequest): + """ ClearOrders request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['power_name'] + params = { + strings.POWER_NAME: parsing.OptionalValueType(str), + } + + def __init__(self, **kwargs): + self.power_name = None # type: str + super(ClearOrders, self).__init__(**kwargs) + +class ClearUnits(_AbstractGameRequest): + """ ClearUnits request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['power_name'] + params = { + strings.POWER_NAME: parsing.OptionalValueType(str), + } + + def __init__(self, **kwargs): + self.power_name = None # type: str + super(ClearUnits, self).__init__(**kwargs) + +class DeleteGame(_AbstractGameRequest): + """ DeleteGame request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = [] + phase_dependent = False + +class GetAllPossibleOrders(_AbstractGameRequest): + """ GetAllPossibleOrders request. + Expected response: response.DataPossibleOrders + Expected response handler result: response.DataPossibleOrders + """ + __slots__ = [] + +class GetPhaseHistory(_AbstractGameRequest): + """ Get a list of game phase data from game history for given phases interval. + A phase can be either None, a phase name (string) or a phase index (integer). + Expected response: responses.DataGamePhases + Expected response handler result: [GamePhaseData objects] + """ + __slots__ = ['from_phase', 'to_phase'] + params = { + strings.FROM_PHASE: parsing.OptionalValueType(parsing.SequenceOfPrimitivesType([str, int])), + strings.TO_PHASE: parsing.OptionalValueType(parsing.SequenceOfPrimitivesType([str, int])), + } + phase_dependent = False + + def __init__(self, **kwargs): + self.from_phase = '' + self.to_phase = '' + super(GetPhaseHistory, self).__init__(**kwargs) + +class LeaveGame(_AbstractGameRequest): + """ LeaveGame request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = [] + +class ProcessGame(_AbstractGameRequest): + """ ProcessGame request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = [] + +class QuerySchedule(_AbstractGameRequest): + """ Query server for info about current scheduling for a game. + Expected response: response.DataGameSchedule + Expected response handler result: response.DataGameSchedule + """ + __slots__ = [] + +class SaveGame(_AbstractGameRequest): + """ Get game saved format in JSON. + Expected response: response.DataSavedGame + Expected response handler result: response.DataSavedGame + """ + __slots__ = [] + +class SendGameMessage(_AbstractGameRequest): + """ SendGameMessage request. + Expected response: responses.DataTimeStamp + Expected response handler result: None + """ + __slots__ = ['message'] + params = { + strings.MESSAGE: parsing.JsonableClassType(Message) + } + + def __init__(self, **kwargs): + self.message = None # type: Message + super(SendGameMessage, self).__init__(**kwargs) + +class SetDummyPowers(_AbstractGameRequest): + """ SetDummyPowers request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['username', 'power_names'] + params = { + strings.USERNAME: parsing.OptionalValueType(str), + strings.POWER_NAMES: parsing.OptionalValueType(parsing.SequenceType(str)), + } + + def __init__(self, **kwargs): + self.username = None + self.power_names = None + super(SetDummyPowers, self).__init__(**kwargs) + +class SetGameState(_AbstractGameRequest): + """ Request to set a game state. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['state', 'orders', 'results', 'messages'] + params = { + strings.STATE: dict, + strings.ORDERS: parsing.DictType(str, parsing.SequenceType(str)), + strings.RESULTS: parsing.DictType(str, parsing.SequenceType(str)), + strings.MESSAGES: parsing.DictType(int, parsing.JsonableClassType(Message), SortedDict.builder(int, Message)), + } + + def __init__(self, **kwargs): + self.state = {} + self.orders = {} + self.results = {} + self.messages = {} # type: SortedDict + super(SetGameState, self).__init__(**kwargs) + +class SetGameStatus(_AbstractGameRequest): + """ SetGameStatus request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['status'] + params = { + strings.STATUS: parsing.EnumerationType(strings.ALL_GAME_STATUSES), + } + + def __init__(self, **kwargs): + self.status = None + super(SetGameStatus, self).__init__(**kwargs) + +class SetOrders(_AbstractGameRequest): + """ SetOrders request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['power_name', 'orders', 'wait'] + params = { + strings.POWER_NAME: parsing.OptionalValueType(str), # required only for game master. + strings.ORDERS: parsing.SequenceType(str), + strings.WAIT: parsing.OptionalValueType(bool) + } + + def __init__(self, **kwargs): + self.power_name = None + self.orders = None + self.wait = None + super(SetOrders, self).__init__(**kwargs) + +class SetWaitFlag(_AbstractGameRequest): + """ SetWaitFlag request. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['power_name', 'wait'] + params = { + strings.POWER_NAME: parsing.OptionalValueType(str), # required only for game master. + strings.WAIT: bool + } + + def __init__(self, **kwargs): + self.power_name = None + self.wait = None + super(SetWaitFlag, self).__init__(**kwargs) + +class Synchronize(_AbstractGameRequest): + """ Synchronize request. + Expected response: responses.DataGameInfo + Expected response handler result: DataGameInfo + """ + __slots__ = ['timestamp'] + params = { + strings.TIMESTAMP: int + } + phase_dependent = False + + def __init__(self, **kwargs): + self.timestamp = None # type: int + super(Synchronize, self).__init__(**kwargs) + +class Vote(_AbstractGameRequest): + """ Vote request. + For powers only. + Allow a power to vote about game draw for current phase. + Expected response: responses.Ok + Expected response handler result: None + """ + __slots__ = ['power_name', 'vote'] + params = { + strings.POWER_NAME: parsing.OptionalValueType(str), + strings.VOTE: strings.ALL_VOTE_DECISIONS + } + + def __init__(self, **kwargs): + self.power_name = '' + self.vote = '' + super(Vote, self).__init__(**kwargs) + +def parse_dict(json_request): + """ Parse a JSON dictionary expected to represent a request. Raise an exception if parsing failed. + :param json_request: JSON dictionary. + :return: a request class instance. + :type json_request: dict + :rtype: _AbstractRequest | _AbstractChannelRequest | _AbstractGameRequest + """ + name = json_request.get(strings.NAME, None) + if name is None: + raise exceptions.RequestException() + expected_class_name = common.snake_case_to_upper_camel_case(name) + request_class = globals().get(expected_class_name, None) + if request_class is None or not inspect.isclass(request_class) or not issubclass(request_class, _AbstractRequest): + raise exceptions.RequestException('Unknown request name %s' % expected_class_name) + try: + return request_class.from_dict(json_request) + except exceptions.DiplomacyException as exc: + LOGGER.error('%s/%s', type(exc).__name__, exc.message) + raise exceptions.RequestException('Wrong request format') diff --git a/diplomacy/communication/responses.py b/diplomacy/communication/responses.py new file mode 100644 index 0000000..a928720 --- /dev/null +++ b/diplomacy/communication/responses.py @@ -0,0 +1,218 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Server -> Client responses sent by server when it received a request. """ +import inspect + +from diplomacy.engine.game import Game +from diplomacy.utils import common, parsing, strings +from diplomacy.utils.scheduler_event import SchedulerEvent +from diplomacy.utils.exceptions import ResponseException +from diplomacy.utils.network_data import NetworkData +from diplomacy.utils.game_phase_data import GamePhaseData + +class _AbstractResponse(NetworkData): + """ Base response object """ + __slots__ = ['request_id'] + header = { + strings.REQUEST_ID: str, + strings.NAME: str, + } + id_field = strings.REQUEST_ID + + def __init__(self, **kwargs): + self.request_id = None + super(_AbstractResponse, self).__init__(**kwargs) + +class Error(_AbstractResponse): + """ Error response sent when an error occurred on server-side while handling a request. """ + __slots__ = ['message'] + params = { + strings.MESSAGE: str + } + + def __init__(self, **kwargs): + self.message = None + super(Error, self).__init__(**kwargs) + +class Ok(_AbstractResponse): + """ Ok response sent by default after handling a request. """ + __slots__ = [] + +class DataGameSchedule(_AbstractResponse): + """ Response with info about current scheduling for a game. """ + __slots__ = ['game_id', 'phase', 'schedule'] + params = { + 'game_id': str, + 'phase': str, + 'schedule': parsing.JsonableClassType(SchedulerEvent) + } + + def __init__(self, **kwargs): + self.game_id = '' + self.phase = '' + self.schedule = None # type: SchedulerEvent + super(DataGameSchedule, self).__init__(**kwargs) + +class DataGameInfo(_AbstractResponse): + """ Response containing information about a game, to be used when no entire game object is required. """ + __slots__ = ['game_id', 'phase', 'timestamp', 'map_name', 'rules', 'status', 'n_players', 'n_controls', + 'deadline', 'registration_password', 'observer_level', 'controlled_powers'] + params = { + strings.GAME_ID: str, + strings.PHASE: str, + strings.TIMESTAMP: int, + strings.MAP_NAME: parsing.OptionalValueType(str), + strings.OBSERVER_LEVEL: parsing.OptionalValueType( + parsing.EnumerationType((strings.MASTER_TYPE, strings.OMNISCIENT_TYPE, strings.OBSERVER_TYPE))), + strings.CONTROLLED_POWERS: parsing.OptionalValueType(parsing.SequenceType(str)), + strings.RULES: parsing.OptionalValueType(parsing.SequenceType(str)), + strings.STATUS: parsing.OptionalValueType(parsing.EnumerationType(strings.ALL_GAME_STATUSES)), + strings.N_PLAYERS: parsing.OptionalValueType(int), + strings.N_CONTROLS: parsing.OptionalValueType(int), + strings.DEADLINE: parsing.OptionalValueType(int), + strings.REGISTRATION_PASSWORD: parsing.OptionalValueType(bool) + } + + def __init__(self, **kwargs): + self.game_id = None # type: str + self.phase = None # type: str + self.timestamp = None # type: int + self.map_name = None # type: str + self.observer_level = None # type: str + self.controlled_powers = None # type: list + self.rules = None # type: list + self.status = None # type: str + self.n_players = None # type: int + self.n_controls = None # type: int + self.deadline = None # type: int + self.registration_password = None # type: bool + super(DataGameInfo, self).__init__(**kwargs) + +class DataPossibleOrders(_AbstractResponse): + """ Response containing a dict of all possibles orders per location and a dict of all orderable locations per power + for a game phase. + """ + __slots__ = ['possible_orders', 'orderable_locations'] + params = { + # {location => [orders]} + strings.POSSIBLE_ORDERS: parsing.DictType(str, parsing.SequenceType(str)), + # {power name => [locations]} + strings.ORDERABLE_LOCATIONS: parsing.DictType(str, parsing.SequenceType(str)), + } + def __init__(self, **kwargs): + self.possible_orders = {} + self.orderable_locations = {} + super(DataPossibleOrders, self).__init__(**kwargs) + +class UniqueData(_AbstractResponse): + """ Response containing only 1 field named `data`. + `params` must have exactly one field named DATA. + """ + __slots__ = ['data'] + + @classmethod + def validate_params(cls): + assert len(cls.params) == 1 and strings.DATA in cls.params + + def __init__(self, **kwargs): + self.data = None + super(UniqueData, self).__init__(**kwargs) + +class DataToken(UniqueData): + """ Unique data containing a token. """ + __slots__ = [] + params = { + strings.DATA: str + } + +class DataMaps(UniqueData): + """ Unique data containing maps info. """ + __slots__ = [] + params = { + # {map_id => {'powers': [power names], 'supply centers' => [supply centers], 'loc_type' => {loc => type}}} + strings.DATA: dict + } + +class DataPowerNames(UniqueData): + """ Unique data containing a list of power names. """ + __slots__ = [] + params = { + strings.DATA: parsing.SequenceType(str) + } + +class DataGames(UniqueData): + """ Unique data containing a list of game info objects. """ + __slots__ = [] + params = { + strings.DATA: parsing.SequenceType(parsing.JsonableClassType(DataGameInfo)) # list of game info. + } + +class DataTimeStamp(UniqueData): + """ Unique data containing a timestamp. """ + __slots__ = [] + params = { + strings.DATA: int # microseconds + } + +class DataGamePhases(UniqueData): + """ Unique data containing a list of GamePhaseData objects. """ + __slots__ = [] + params = { + strings.DATA: parsing.SequenceType(parsing.JsonableClassType(GamePhaseData)) + } + +class DataGame(UniqueData): + """ Unique data containing a Game object. """ + __slots__ = [] + params = { + strings.DATA: parsing.JsonableClassType(Game) + } + +class DataSavedGame(UniqueData): + """ Unique data containing a game saved in JSON format. """ + __slots__ = [] + params = { + strings.DATA: dict + } + +class DataGamesToPowerNames(UniqueData): + """ Unique data containing a dict of game IDs associated to sequences of power names. """ + __slots__ = [] + params = { + strings.DATA: parsing.DictType(str, parsing.SequenceType(str)) + } + +def parse_dict(json_response): + """ Parse a JSON dictionary expected to represent a response. + Raise an exception if either: + - parsing failed + - response received is an Error response. In such case, a ResponseException is raised + with the error message. + :param json_response: a JSON dict. + :return: a Response class instance. + """ + assert isinstance(json_response, dict), 'Response parser expects a dict.' + name = json_response.get(strings.NAME, None) + if name is None: + raise ResponseException() + expected_class_name = common.snake_case_to_upper_camel_case(name) + response_class = globals()[expected_class_name] + assert inspect.isclass(response_class) and issubclass(response_class, _AbstractResponse) + response_object = response_class.from_dict(json_response) + if isinstance(response_object, Error): + raise ResponseException('%s: %s' % (response_object.name, response_object.message)) + return response_object diff --git a/diplomacy/engine/__init__.py b/diplomacy/engine/__init__.py new file mode 100644 index 0000000..4f2769f --- /dev/null +++ b/diplomacy/engine/__init__.py @@ -0,0 +1,16 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== diff --git a/diplomacy/engine/game.py b/diplomacy/engine/game.py new file mode 100644 index 0000000..73b2ff9 --- /dev/null +++ b/diplomacy/engine/game.py @@ -0,0 +1,4289 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +# -*- coding: utf-8 -*- +""" Game + - Contains the game engine +""" +# pylint: disable=too-many-lines +import os +import sys +import time +import uuid +import random +from copy import deepcopy + +import numpy as np + +from diplomacy import settings +import diplomacy.utils.errors as err +from diplomacy.engine.map import Map +from diplomacy.engine.message import Message, GLOBAL +from diplomacy.engine.power import Power +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.game_phase_data import GamePhaseData, MESSAGES_TYPE + +# Constants +UNDETERMINED, POWER, UNIT, LOCATION, COAST, ORDER, MOVE_SEP, OTHER = 0, 1, 2, 3, 4, 5, 6, 7 + +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}]} + - 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'] + - 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 + e.g. map_name = 'standard' + - 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 + """ + # pylint: disable=too-many-instance-attributes + __slots__ = ['victory', 'no_rules', 'meta_rules', 'phase', 'note', 'map', 'powers', 'outcome', 'error', 'popped', + 'messages', 'order_history', 'orders', 'ordered_units', 'phase_type', 'win', 'combat', 'command', + 'result', 'supports', 'dislodged', 'lost', 'convoy_paths', 'convoy_paths_possible', + 'convoy_paths_dest', 'zobrist_hash', 'renderer', 'game_id', 'map_name', 'role', 'rules', + 'message_history', 'state_history', 'result_history', 'status', 'timestamp_created', 'n_controls', + 'deadline', 'registration_password', 'observer_level', 'controlled_powers', '_phase_wrapper_type', + 'phase_abbr'] + zobrist_tables = {} + rule_cache = () + model = { + strings.CONTROLLED_POWERS: parsing.OptionalValueType(parsing.SequenceType(str)), + strings.DEADLINE: parsing.DefaultValueType(int, 300), + strings.ERROR: parsing.DefaultValueType(parsing.SequenceType(str), []), + strings.GAME_ID: parsing.OptionalValueType(str), + strings.MAP_NAME: parsing.DefaultValueType(str, 'standard'), + strings.MESSAGE_HISTORY: parsing.DefaultValueType(parsing.DictType(str, MESSAGES_TYPE), {}), + strings.MESSAGES: parsing.DefaultValueType(MESSAGES_TYPE, []), + strings.META_RULES: parsing.DefaultValueType(parsing.SequenceType(str), []), + strings.N_CONTROLS: parsing.OptionalValueType(int), + strings.NO_RULES: parsing.DefaultValueType(parsing.SequenceType(str, set), []), + strings.NOTE: parsing.DefaultValueType(str, ''), + strings.OBSERVER_LEVEL: parsing.OptionalValueType( + parsing.EnumerationType((strings.MASTER_TYPE, strings.OMNISCIENT_TYPE, strings.OBSERVER_TYPE))), + strings.ORDER_HISTORY: parsing.DefaultValueType( + parsing.DictType(str, parsing.DictType(str, parsing.SequenceType(str))), {}), + strings.OUTCOME: parsing.DefaultValueType(parsing.SequenceType(str), []), + strings.PHASE: parsing.DefaultValueType(str, ''), + strings.PHASE_ABBR: parsing.DefaultValueType(str, ''), + strings.POWERS: parsing.DefaultValueType(parsing.DictType(str, parsing.JsonableClassType(Power)), {}), + strings.REGISTRATION_PASSWORD: parsing.OptionalValueType(str), + strings.RESULT_HISTORY: parsing.DefaultValueType(parsing.DictType(str, parsing.DictType( + str, parsing.SequenceType(parsing.EnumerationType( + ['no convoy', 'bounce', 'void', 'cut', 'dislodged', 'disrupted', 'disband', ''])))), {}), + strings.ROLE: parsing.DefaultValueType(str, strings.SERVER_TYPE), + strings.RULES: parsing.DefaultValueType(parsing.SequenceType(str, sequence_builder=list), ()), + strings.STATE_HISTORY: parsing.DefaultValueType(parsing.DictType(str, dict), {}), + strings.STATUS: parsing.DefaultValueType(parsing.EnumerationType(strings.ALL_GAME_STATUSES), strings.FORMING), + strings.TIMESTAMP_CREATED: parsing.OptionalValueType(int), + strings.VICTORY: parsing.DefaultValueType(parsing.SequenceType(int), []), + strings.WIN: parsing.DefaultValueType(int, 0), + strings.ZOBRIST_HASH: parsing.DefaultValueType(parsing.StringableType(np.int64), '0'), + } + + def __init__(self, game_id=None, **kwargs): + """ Constructor """ + self.victory = None + self.no_rules = set() + self.meta_rules = [] + self.phase, self.note = '', '' + self.map = None # type: Map + self.powers = {} + self.outcome, self.error, self.popped = [], [], [] + self.orders, self.ordered_units = {}, {} + self.phase_type = None + self.win = None + self.combat, self.command, self.result = {}, {}, {} + self.supports, self.dislodged, self.lost = {}, {}, {} + self.convoy_paths, self.convoy_paths_possible, self.convoy_paths_dest = {}, None, None + self.zobrist_hash = 0 + self.renderer = None + self.game_id = None # type: str + self.map_name = None # type: str + self.messages = None # type: SortedDict + self.role = None # type: str + self.rules = [] + self.state_history, self.order_history, self.result_history, self.message_history = {}, {}, {}, {} + self.status = None # type: str + self.timestamp_created = None # type: int + self.n_controls = None + self.deadline = 0 + self.registration_password = None + self.observer_level = None + self.controlled_powers = None + + # Remove rules from kwargs (if present), as we want to add them manually using self.add_rule(). + rules = kwargs.pop(strings.RULES, None) + + # Update rules with game ID. + kwargs[strings.GAME_ID] = game_id + + # Initialize game with kwargs. + super(Game, self).__init__(**kwargs) + + # Check settings. + if self.registration_password is not None and self.registration_password == '': + raise exceptions.DiplomacyException('Registration password must be None or non-empty string.') + if self.n_controls is not None and self.n_controls < 0: + 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'] + # 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 + elif self.n_controls == 0: + # If number of allowed players is 0, the game can only be solitaire. + self.add_rule('SOLITAIRE') + + # Check timestamp_created. + if self.timestamp_created is None: + self.timestamp_created = common.timestamp_microseconds() + + # Check game ID. + if self.game_id is None: + self.game_id = '%s/%s' % (self.timestamp_created, uuid.uuid4()) + + # Validating status + self._validate_status(reinit_powers=(self.timestamp_created is None)) + + if self.powers: + # Game loaded with powers. + # Associate loaded powers with this game. + for power in self.powers.values(): + power.game = self + else: + # Begin game. + self._begin() + + # Game loaded. + + # Check map powers. + assert all(self.has_power(power_name) for power_name in self.map.powers) + + # Check role and consistency between all power roles and game role. + if self.has_power(self.role): + # It's a power game. Each power must be a player power. + assert all(power.role == power.name for power in self.powers.values()) + else: + # We should have a non-power game and each power must have same role as game role. + assert self.role in strings.ALL_ROLE_TYPES + assert all(power.role == self.role for power in self.powers.values()) + + # Wrap history fields into runtime sorted dictionaries. + # This is necessary to sort history fields by phase name. + + self._phase_wrapper_type = common.str_cmp_class(self.map.compare_phases) + + self.order_history = SortedDict(self._phase_wrapper_type, dict, + {self._phase_wrapper_type(key): value + for key, value in self.order_history.items()}) + self.message_history = SortedDict(self._phase_wrapper_type, SortedDict, + {self._phase_wrapper_type(key): value + for key, value in self.message_history.items()}) + self.state_history = SortedDict(self._phase_wrapper_type, dict, + {self._phase_wrapper_type(key): value + for key, value in self.state_history.items()}) + self.result_history = SortedDict(self._phase_wrapper_type, dict, + {self._phase_wrapper_type(key): value + for key, value in self.result_history.items()}) + + def __str__(self): + """ Returns a string representation of the game instance """ + show_map = self.map + show_result = self.outcome + + text = '' + text += 'GAME %s%s%s' % (self.game_id, '\nPHASE ', self.phase) + text += '\nMAP %s' % self.map_name if show_map else '' + text += '\nRESULT %s' % ' '.join(self.outcome) if show_result else '' + text += '\nRULE '.join([''] + [rule for rule in self.rules if rule not in self.meta_rules]) + text += '\nRULE !'.join([''] + [no_rule for no_rule in self.no_rules]) + return text + + def __deepcopy__(self, memo): + """ Fast deep copy implementation """ + cls = self.__class__ + result = cls.__new__(cls) + + # Deep copying + for key in self._slots: + if key in ['map', 'renderer', 'powers']: + continue + setattr(result, key, deepcopy(getattr(self, key))) + setattr(result, 'map', self.map) + setattr(result, 'powers', {}) + for power in self.powers.values(): + result.powers[power.name] = deepcopy(power) + setattr(result.powers[power.name], 'game', result) + return result + + # ==================================================================== + # Public Interface + # ==================================================================== + + @property + def _slots(self): + """ Return an iterable of all attributes of this object. + Should be used in place of "self.__slots__" to be sure to retrieve all + attribute names from a derived class (including parent slots). + """ + return (name for cls in type(self).__mro__ for name in getattr(cls, '__slots__', ())) + + @property + def power(self): + """ (only for player games) Return client power associated to this game. + :return: a Power object. + :rtype: Power + """ + return self.powers[self.role] if self.is_player_game() else None + + @property + def is_game_done(self): + """ Returns a boolean flag that indicates if the game is done """ + return self.phase == 'COMPLETED' + + is_game_forming = property(lambda self: self.status == strings.FORMING) + is_game_active = property(lambda self: self.status == strings.ACTIVE) + is_game_paused = property(lambda self: self.status == strings.PAUSED) + is_game_canceled = property(lambda self: self.status == strings.CANCELED) + is_game_completed = property(lambda self: self.status == strings.COMPLETED) + current_short_phase = property(lambda self: self.map.phase_abbr(self.phase, self.phase)) + + civil_disorder = property(lambda self: 'CIVIL_DISORDER' in self.rules) + multiple_powers_per_player = property(lambda self: 'MULTIPLE_POWERS_PER_PLAYER' in self.rules) + no_observations = property(lambda self: 'NO_OBSERVATIONS' in self.rules) + no_press = property(lambda self: 'NO_PRESS' in self.rules) + power_choice = property(lambda self: 'POWER_CHOICE' in self.rules) + public_press = property(lambda self: 'PUBLIC_PRESS' in self.rules) + real_time = property(lambda self: 'REAL_TIME' in self.rules) + start_master = property(lambda self: 'START_MASTER' in self.rules) + solitaire = property(lambda self: 'SOLITAIRE' in self.rules) + + # ============================================================== + # Application/network methods (mainly used for connected games). + # ============================================================== + + def is_player_game(self): + """ Return True if this game is a player game. """ + return self.has_power(self.role) + + def is_observer_game(self): + """ Return True if this game is an observer game. """ + return self.role == strings.OBSERVER_TYPE + + def is_omniscient_game(self): + """ Return True if this game is an omniscient game. """ + return self.role == strings.OMNISCIENT_TYPE + + def is_server_game(self): + """ Return True if this game is a server game. """ + return self.role == strings.SERVER_TYPE + + def is_valid_password(self, registration_password): + """ Return True if given plain password matches registration password. """ + if self.registration_password is None: + return registration_password is None + if registration_password is None: + return False + 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 self.get_power(power_name).is_controlled() + + def is_dummy(self, power_name): + """ Return True if given power name is not currently controlled. """ + return not self.is_controlled(power_name) + + def does_not_wait(self): + """ Return True if the game does not wait anything to process its current phase. + The game is not waiting is all **controlled** powers have defined orders and wait flag set to False. + If it's a solitaire game (with no controlled powers), all (dummy, not eliminated) powers must have defined + orders and wait flag set to False. By default, wait flag for a dummy power is True. + Note that an empty orders set is considered as a defined order as long as it was + explicitly set by the power controller. + """ + if any(power.is_controlled() for power in self.powers.values()): + return all(power.does_not_wait() for power in self.powers.values() if power.is_controlled()) + return all(power.is_eliminated() or power.does_not_wait() for power in self.powers.values()) + + def has_power(self, power_name): + """ Return True if this game has given power name. """ + return power_name in self.map.powers + + def has_expected_controls_count(self): + """ Return True if game has expected number of map powers to be controlled. + If True, the game can start (if not yet started). + """ + return self.count_controlled_powers() == self.get_expected_controls_count() + + def count_controlled_powers(self): + """ Return the number of controlled map powers. """ + return sum(1 for power_name in self.get_map_power_names() if self.is_controlled(power_name)) + + def get_controlled_power_names(self, username): + """ Return the list of power names currently controlled by given user name. """ + return [power.name for power in self.powers.values() if power.is_controlled_by(username)] + + def get_expected_controls_count(self): + """ Return the number of map powers expected to be controlled in this game. + This number is either specified in settings or the number of map powers. + """ + expected_count = self.n_controls + if expected_count is None: + 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 objects. """ + return set(power_name for power_name in self.get_map_power_names() if self.is_dummy(power_name)) + + def get_controllers(self): + """ Return a dictionary mapping each power name to its current controller name.""" + return {power.name: power.get_controller() for power in self.powers.values()} + + def get_controllers_timestamps(self): + """ Return a dictionary mapping each power name to its controller timestamp. """ + return {power.name: power.get_controller_timestamp() for power in self.powers.values()} + + def get_random_power_name(self): + """ Return a random power name from remaining dummy power names. + Raise an exception if there are no dummy power names. + """ + playable_power_names = list(self.get_dummy_power_names()) + if not playable_power_names: + raise exceptions.RandomPowerException(1, len(playable_power_names)) + playable_power_names.sort() + 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: a timestamp + :rtype: int + """ + timestamp = self.timestamp_created + if self.state_history: + timestamp = max(self.state_history.last_value()['timestamp'], timestamp) + if self.messages: + timestamp = max(self.messages.last_key(), timestamp) + return timestamp + + @classmethod + 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. + :param timestamp_from: lower timestamp (included) for required messages. + :param timestamp_to: upper timestamp (included) for required messages. + :return: a dict of corresponding messages (empty if no corresponding messages found), + mapping messages timestamps to messages. + :type messages: diplomacy.utils.sorted_dict.SortedDict + """ + + # Observer can see global messages and system messages sent to observers. + if isinstance(game_role, str) and game_role == strings.OBSERVER_TYPE: + return {message.time_sent: message + for message in messages.sub(timestamp_from, timestamp_to) + if message.is_global() or message.for_observer()} + + # Omniscient observer can see all messages. + if isinstance(game_role, str) and game_role == strings.OMNISCIENT_TYPE: + return {message.time_sent: message + for message in messages.sub(timestamp_from, timestamp_to)} + + # Power can see global messages and all messages she sent or received. + if isinstance(game_role, str): + game_role = [game_role] + elif not isinstance(game_role, list): + game_role = list(game_role) + return {message.time_sent: message + for message in messages.sub(timestamp_from, timestamp_to) + if message.is_global() or message.recipient in game_role or message.sender in game_role} + + 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. + If none, messages in game history will not be filtered. + """ + if isinstance(from_phase, int): + from_phase = self.state_history.key_from_index(from_phase) + elif isinstance(from_phase, str): + from_phase = self._phase_wrapper_type(from_phase) + if isinstance(to_phase, int): + to_phase = self.state_history.key_from_index(to_phase) + elif isinstance(to_phase, str): + to_phase = self._phase_wrapper_type(to_phase) + phases = self.state_history.sub_keys(from_phase, to_phase) + states = self.state_history.sub(from_phase, to_phase) + orders = self.order_history.sub(from_phase, to_phase) + messages = self.message_history.sub(from_phase, to_phase) + results = self.result_history.sub(from_phase, to_phase) + if game_role: + messages = [self.filter_messages(msg_dict, game_role) for msg_dict in messages] + assert len(phases) == len(states) == len(orders) == len(messages) == len(results), ( + len(phases), len(states), len(orders), len(messages), len(results)) + return [GamePhaseData(name=str(phases[i]), + state=states[i], + orders=orders[i], + messages=messages[i], + results=results[i]) + for i in range(len(phases))] + + def get_phase_from_history(self, short_phase_name, game_role=None): + """ Return a game phase data corresponding to given phase from phase history. """ + return self.get_phase_history(short_phase_name, short_phase_name, game_role)[0] + + def phase_history_from_timestamp(self, timestamp): + """ Return list of game phase data from game history for which state timestamp >= given timestamp. """ + earliest_phase = '' + for state in self.state_history.reversed_values(): + if state['timestamp'] < timestamp: + break + earliest_phase = state['name'] + return self.get_phase_history(from_phase=earliest_phase) if earliest_phase else [] + + 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 + """ + phase = self._phase_wrapper_type(game_phase_data.name) + assert phase not in self.state_history + assert phase not in self.message_history + assert phase not in self.order_history + assert phase not in self.result_history + self.state_history.put(phase, game_phase_data.state) + self.message_history.put(phase, game_phase_data.messages) + self.order_history.put(phase, game_phase_data.orders) + self.result_history.put(phase, game_phase_data.results) + + def set_status(self, status): + """ Set game status with given status (should be in diplomacy.utils.strings.ALL_GAME_STATUSES). """ + assert status in strings.ALL_GAME_STATUSES + self.status = status + + 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. + """ + if winners is None: + # Draw with all powers which still have units in map. + winners = [power.name for power in self.powers.values() if power.units] + + # No orders will be processed when drawing, so clear current orders. + self.clear_orders() + + # Collect data about current phase before drawing. + previous_phase = self._phase_wrapper_type(self.current_short_phase) + previous_orders = self.get_orders() + previous_messages = self.messages.copy() + previous_state = self.get_state() + + # Finish the game. + self._finish(winners) + + # Then clear game and save previous phase. + self.clear_vote() + self.clear_orders() + self.messages.clear() + self.order_history.put(previous_phase, previous_orders) + self.message_history.put(previous_phase, previous_messages) + self.state_history.put(previous_phase, previous_state) + self.result_history.put(previous_phase, {}) + + # There are no expected results for orders, as there are no orders processed. + + previous_phase_data = GamePhaseData(name=str(previous_phase), + state=previous_state, + orders=previous_orders, + messages=previous_messages, + results={}) + current_phase_data = GamePhaseData(name=self.current_short_phase, + state=self.get_state(), + orders={}, + messages={}, + results={}) + + return previous_phase_data, current_phase_data + + def set_controlled(self, power_name, username): + """ Control power with given username (may be None to set dummy power). + See method diplomacy.Power#set_controlled. + """ + self.get_power(power_name).set_controlled(username) + + 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: + if self.has_power(dummy_power_name): + self.set_controlled(dummy_power_name, None) + + 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. + :type powers_controllers: dict + """ + for power_name, controller in powers_controllers.items(): + self.get_power(power_name).update_controller(controller, timestamps[power_name]) + + def new_power_message(self, recipient, body): + """ 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. + :rtype: GameMessage + """ + assert self.is_player_game() + if not self.has_power(recipient): + raise exceptions.MapPowerException(recipient) + return Message(phase=self.current_short_phase, sender=self.role, recipient=recipient, message=body) + + 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 + """ + assert self.is_player_game() + return Message(phase=self.current_short_phase, sender=self.role, recipient=GLOBAL, message=body) + + def add_message(self, message): + """ 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 + """ + assert isinstance(message, Message) + if self.is_player_game(): + assert message.is_global() or self.power.name in (message.sender, message.recipient) + + if message.time_sent is None: + # This instance must be a server game. + # Message should be a new message matching current game phase. + # There should not be any more recent message in message history (as we are adding a new message). + # We must generate a timestamp for this message. + assert self.is_server_game() + if message.phase != self.current_short_phase: + raise exceptions.GamePhaseException(self.current_short_phase, message.phase) + assert not self.messages or common.timestamp_microseconds() >= self.messages.last_key() + time.sleep(1e-6) + message.time_sent = common.timestamp_microseconds() + + self.messages.put(message.time_sent, message) + return message.time_sent + + # Vote methods. For server and omniscient games only. + # Observer game should not see votes. + # Power game should know only vote of related power (votes for all other power should be 'neutral' in a power game). + + def has_draw_vote(self): + """ Return True if all controlled non-eliminated powers have voted YES to draw game at current phase. """ + assert self.is_server_game() or self.is_omniscient_game() + return all( + power.vote == strings.YES + for power in self.powers.values() + if not power.is_eliminated() + ) + + def count_voted(self): + """ Return the count of controlled powers who already voted for a draw for current phase. """ + assert self.is_server_game() or self.is_omniscient_game() + return sum(1 for power in self.powers.values() + if not power.is_eliminated() and power.vote != strings.NEUTRAL) + + def clear_vote(self): + """ Clear current vote. """ + for power in self.powers.values(): + power.vote = strings.NEUTRAL + + # ============== + # Basic methods. + # ============== + 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') + """ + if power_name is not None: + power_name = power_name.upper() + power = self.get_power(power_name) + if power_name is not None: + return power.units[:] + ['*{}'.format(unit) for unit in power.retreats] + if power_name is None: + units = {} + for power in self.powers.values(): + units[power.name] = self.get_units(power.name) + return units + return [] + + 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': [...], ...} + """ + if power_name is not None: + power_name = power_name.upper() + power = self.get_power(power_name) + if power_name is not None: + return power.centers[:] + if power_name is None: + centers = {} + for power in self.powers.values(): + centers[power.name] = self.get_centers(power.name) + return centers + return [] + + 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', ...], ...} + """ + if power_name is not None: + power_name = power_name.upper() + power = self.get_power(power_name) + + # Getting orders for a particular power + # Skipping VOID and WAIVE orders in Adjustment/Retreats phase + if power_name is not None: + if self.get_current_phase()[-1] == 'M': + if 'NO_CHECK' in self.rules: + power_orders = [power.orders[order] for order in power.orders if power.orders[order]] + else: + power_orders = ['{} {}'.format(unit, unit_order) for unit, unit_order in power.orders.items()] + else: + power_orders = [order for order in power.adjust + if order and order != 'WAIVE' and order.split()[0] != 'VOID'] + return power_orders + + # Recursively calling itself to get all powers + elif power_name is None: + orders = {} + for power in self.powers.values(): + orders[power.name] = self.get_orders(power.name) + return orders + return [] + + 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': [...], ...} + """ + if power_name is not None: + power_name = power_name.upper() + power = self.get_power(power_name) + + # Single power + if power_name is not None: + current_phase_type = self.get_current_phase()[-1] + + # Adjustment + if current_phase_type == 'A': + build_count = len(power.centers) - len(power.units) + + # Building - All unoccupied homes + if build_count > 0: + orderable_locs = self._build_sites(power) + + # Nothing can be built. + elif build_count == 0: + orderable_locs = [] + + # Disbanding - All units location + else: + orderable_locs = [unit[2:5] for unit in power.units] + + # Retreating + elif current_phase_type == 'R': + orderable_locs = [unit[2:5] for unit in power.retreats] + + # Movement + else: + orderable_locs = [unit[2:5] for unit in power.units] + + # Returning and sorting for deterministic output + return sorted(orderable_locs) + + # All powers + else: + return {power.name: self.get_orderable_locations(power.name) for power in self.powers.values()} + + def get_order_status(self, power_name=None, unit=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 + :param unit: Optional. If provided (e.g. 'A PAR') will only return that specific unit order status. + :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 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, returning string + if unit is not None: + result_dict = self.result_history.last_value() if self.result_history else {} + return result_dict[unit][:] if unit in result_dict else [] + + # Specific power, returning dictionary + if power_name is not None: + power_name = power_name.upper() + if power_name is not None: + order_status = {} + if self.state_history: + state_history = self.state_history.last_value() + for ordered_unit in state_history['units'][power_name]: + ordered_unit = ordered_unit.replace('*', '') + order_status[ordered_unit] = self.get_order_status(power_name, ordered_unit) + return order_status + + # All powers + if power_name is None: + order_status = {} + for power in self.powers.values(): + order_status[power.name] = self.get_order_status(power.name) + return order_status + return {} + + 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 + """ + 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 + :param reset: Boolean. If, clear all units of the power before setting them + :return: Nothing + """ + power_name = power_name.upper() + if not isinstance(units, list): + units = [units] + if power_name not in self.powers: + return + + # Clearing old units if reset is true + if reset and power_name in self.powers: + self.powers[power_name].clear_units() + + regular_units = [unit for unit in units if unit[0] != '*'] + dislodged_units = [unit[1:] for unit in units if unit[0] == '*'] + influence = [unit[2:5] for unit in regular_units + dislodged_units] + + # Removing units that are already there + for power in self.powers.values(): + for unit in regular_units: + unit_loc = unit[2:5] + for unit_to_remove in [p_unit for p_unit in power.units if p_unit[2:5] == unit_loc]: + self.update_hash(power.name, unit_type=unit_to_remove[0], loc=unit_to_remove[2:]) + power.units.remove(unit_to_remove) + for unit in dislodged_units: + unit_loc = unit[2:5] + for unit_to_remove in [p_unit for p_unit in power.retreats if p_unit[2:5] == unit_loc]: + self.update_hash(power.name, unit_type=unit_to_remove[0], loc=unit_to_remove[2:], is_dislodged=True) + del power.retreats[unit_to_remove] + for loc in influence: + if loc in power.influence: + power.influence.remove(loc) + + # Retrieving the target power + power = self.get_power(power_name) + + # Re-adding normal units to the new power + for unit in regular_units: + word = unit.upper().split() + if len(word) != 2: + continue + unit_type, unit_loc = word + if unit_type in ('A', 'F') \ + and unit_loc in [loc.upper() for loc in self.map.locs] \ + and self.map.is_valid_unit(unit): + if power: + self.update_hash(power_name, unit_type=unit_type, loc=unit_loc) + power.units.append(unit) + power.influence.append(unit[2:5]) + else: + self.error += [err.MAP_INVALID_UNIT % unit] + + # Re-adding dislodged units to the new power + for unit in dislodged_units: + word = unit.upper().split() + if len(word) != 2: + continue + unit_type, unit_loc = word + if unit_type in ('A', 'F') and unit_loc in [loc.upper() for loc in self.map.locs]: + abuts = [abut.upper() for abut in self.map.abut_list(unit_loc, incl_no_coast=True) + if self._abuts(unit_type, unit_loc, '-', abut.upper())] + if power: + self.update_hash(power_name, unit_type=unit_type, loc=unit_loc, is_dislodged=True) + power.retreats[unit] = abuts + + # Clearing cache + self.clear_cache() + + 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 + :return: Nothing + """ + power_name = power_name.upper() + if not isinstance(centers, list): + centers = [centers] + if power_name not in self.powers: + return + + # Clearing old centers if reset is true + if reset and power_name in self.powers: + self.powers[power_name].clear_centers() + + # Removing centers that are already controlled by another power + for power in self.powers.values(): + for center in centers: + if center in power.centers: + self.update_hash(power.name, loc=center, is_center=True) + power.centers.remove(center) + + # Transferring center to power_name + power = self.get_power(power_name) + if power: + for center in centers: + if center in self.map.scs: + self.update_hash(power_name, loc=center, is_center=True) + power.centers += [center] + + # Clearing cache + self.clear_cache() + + 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. + :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 + """ + power_name = power_name.upper() + + if not self.has_power(power_name): + return + + if self.is_player_game() and self.role != power_name: + return + + power = self.get_power(power_name) + if power: + if not isinstance(orders, list): + orders = [orders] + + # Remove any empty string from orders. + orders = [order for order in orders if order] + + # Setting orders depending on phase type + if self.phase_type == 'R': + self._update_retreat_orders(power, orders, expand=expand, replace=replace) + elif self.phase_type == 'A': + self._update_adjust_orders(power, orders, expand=expand, replace=replace) + else: + self._update_orders(power, orders, expand=expand, replace=replace) + power.order_is_set = (OrderSettings.ORDER_SET + if self.get_orders(power.name) + else OrderSettings.ORDER_SET_EMPTY) + + 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). + """ + power_name = power_name.upper() + + if not self.has_power(power_name): + return + + power = self.get_power(power_name.upper()) # type: 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 + :return: Nothing + """ + for power in self.powers.values(): + if power_name is None or power.name == power_name: + power.clear_units() + self.clear_cache() + + 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. + :return: Nothing + """ + for power in self.powers.values(): + if power_name is None or power.name == power_name: + power.clear_centers() + self.clear_cache() + + 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. + :return: Nothing + """ + if power_name is not None: + power = self.get_power(power_name.upper()) + power.clear_orders() + else: + for power in self.powers.values(): + power.clear_orders() + + def clear_cache(self): + """ Clears all caches """ + self.convoy_paths_possible, self.convoy_paths_dest = None, 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'): + self.phase = new_phase + self.phase_type = None + else: + self.phase = self.map.phase_long(new_phase) + self.phase_type = self.phase.split()[-1][0] + + 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. + :return: The rendered image in the specified format. + """ + if not self.renderer: + self.renderer = Renderer(self) + return self.renderer.render(incl_orders=incl_orders, incl_abbrev=incl_abbrev, output_format=output_format) + + 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 + """ + if not self.__class__.rule_cache: + self._load_rules() + valid_rules = {valid_rule for valid_rule in self.__class__.rule_cache[0]} + + if rule not in valid_rules or rule in self.no_rules: + return + + forbidden_rules = self.__class__.rule_cache[0].get(rule, {}).get('!', []) + rules_to_add = self.__class__.rule_cache[0].get(rule, {}).get('+', []) + rules_to_remove = self.__class__.rule_cache[0].get(rule, {}).get('-', []) + + # Making sure we don't already have a forbidden rule + for forbidden in forbidden_rules: + if forbidden in self.rules: + self.error += [err.GAME_FORBIDDEN_RULE % (forbidden, rule)] + return + if forbidden not in self.no_rules: + self.no_rules.add(forbidden) + + # Adding rules + for rule_to_add in rules_to_add: + if rule_to_add not in self.rules: + self.rules.append(rule_to_add) + + # Removing rules + for rule_to_remove in rules_to_remove: + if rule_to_remove in self.rules: + self.rules.remove(rule_to_remove) + + # Adding main rule + if rule not in self.rules: + self.rules.append(rule) + + 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 + """ + if rule in self.rules: + self.rules.remove(rule) + + 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 + """ + # Create a map, and check for errors + self.map = Map(self.map_name) + if self.map_name != self.map.name: + raise RuntimeError('Invalid Map loaded. Expected %s - Got %s' % (self.map_name, self.map.name)) + + # Adding map rules + for rule in self.map.rules: + self.add_rule(rule) + + # Build Zobrist tables + self._build_hash_table() + + self.error += self.map.error + + # Sets the current phase to the long version + if self.phase and ' ' not in self.phase and self.phase not in ('FORMING', 'COMPLETED'): + self.phase = self.map.phase_long(self.phase) + + # Have the Game process all lines in the map file that were in DIRECTIVES clauses (this includes any RULE lines) + # Do this for all directives given without a variant and for those specific for this Game's variant. + if self.phase == 'FORMING': + return + + # Resetting powers + if reinit_powers: + self.powers = {} + + 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) + previous_orders = self.get_orders() + previous_messages = self.messages.copy() + previous_state = self.get_state() + + if self.error: + if 'IGNORE_ERRORS' not in self.rules: + print('The following errors were encountered and were cleared before processing.') + for error in self.error: + print('-- %s' % error) + print('-' * 32) + self.error = [] + self._process() + + # result_history should have been updated with orders results for processed (previous) phase. + + self.clear_vote() + self.clear_orders() + self.messages.clear() + self.order_history.put(previous_phase, previous_orders) + self.message_history.put(previous_phase, previous_messages) + self.state_history.put(previous_phase, previous_state) + return GamePhaseData(name=str(previous_phase), + state=previous_state, + orders=previous_orders, + messages=previous_messages, + results=self.result_history[previous_phase]) + + def rebuild_hash(self): + """ Completely recalculate the Zobrist hash + :return: The updated hash value + """ + self.zobrist_hash = 0 + if self.map is None: + return 0 + + # Recalculating for each power + for power in self.powers.values(): + for unit in power.units: + self.update_hash(power.name, unit_type=unit[0], loc=unit[2:]) + for dis_unit in power.retreats: + self.update_hash(power.name, unit_type=dis_unit[0], loc=dis_unit[2:], is_dislodged=True) + for center in power.centers: + self.update_hash(power.name, loc=center, is_center=True) + for home in power.homes: + self.update_hash(power.name, loc=home, is_home=True) + + # Clearing cache + self.clear_cache() + + # Returning the new hash + return self.get_hash() + + def get_hash(self): + """ Returns the zobrist hash for the current game """ + # Needs to be a string, otherwise json.dumps overflows + return str(self.zobrist_hash) + + 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 + """ + if self.map is None: + return + zobrist = self.__class__.zobrist_tables[self.map_name] + loc = loc[:3].upper() if is_center or is_home else loc.upper() + power = power.upper() + + power_ix = zobrist['map_powers'].index(power) + loc_ix = zobrist['map_locs'].index(loc) + unit_type_ix = ['A', 'F'].index(unit_type) if unit_type in ['A', 'F'] else -1 + + # Dislodged + if is_dislodged: + self.zobrist_hash ^= zobrist['dis_unit_type'][unit_type_ix, loc_ix] + self.zobrist_hash ^= zobrist['dis_units'][power_ix, loc_ix] + + # Supply Center + elif is_center: + self.zobrist_hash ^= zobrist['centers'][power_ix, loc_ix] + + # Home + elif is_home: + self.zobrist_hash ^= zobrist['homes'][power_ix, loc_ix] + + # Regular unit + else: + self.zobrist_hash ^= zobrist['unit_type'][unit_type_ix, loc_ix] + self.zobrist_hash ^= zobrist['units'][power_ix, loc_ix] + + def get_phase_data(self): + """ Return a GamePhaseData object representing current game. """ + # Associate each power name to power orders, or None if order ist not set for the power. + # This is done to make distinction between voluntary empty orders ([]) and unset orders (None). + current_orders = {power.name: (self.get_orders(power.name) if power.order_is_set else None) + for power in self.powers.values()} + # Game does not have results for current orders (until orders are processed and game phase is updated). + return GamePhaseData(name=self.current_short_phase, + state=self.get_state(), + orders=current_orders, + messages=self.messages.copy(), + results={}) + + 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. + Previous phase data in given list will replace current game history. + :param clear_history: Indicate if we must clear game history fields before update. + """ + if not phase_data: + return + if isinstance(phase_data, GamePhaseData): + phase_data = [phase_data] + elif not isinstance(phase_data, list): + phase_data = list(phase_data) + + if clear_history: + self._clear_history() + + for game_phase_data in phase_data[:-1]: # type: GamePhaseData + self.extend_phase_history(game_phase_data) + + current_phase_data = phase_data[-1] # type: GamePhaseData + self.set_state(current_phase_data.state, clear_history=False) + for power_name, power_orders in current_phase_data.orders.items(): + if power_orders is not None: + self.set_orders(power_name, power_orders) + self.messages = current_phase_data.messages.copy() + # We ignore 'results' for current phase data. + + def get_state(self): + """ Gets the internal saved state of the game. + This state is intended to represent current game view + (powers states, orders results for previous phase, and few more info). + See field message_history to get messages from previous phases. + 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. + :return: The internal saved state (dict) of the game + """ + state = {} + state['timestamp'] = common.timestamp_microseconds() + state['zobrist_hash'] = self.get_hash() + state['note'] = self.note + state['name'] = self._phase_abbr() + state['units'] = {} + state['centers'] = {} + state['homes'] = {} + state['influence'] = {} + state['civil_disorder'] = {} + state['builds'] = {} + + # Setting powers data: units, centers, homes, influence and civil disorder. + for power in self.powers.values(): + state['units'][power.name] = list(power.units) + ['*{}'.format(d) for d in power.retreats] + state['centers'][power.name] = list(power.centers) + state['homes'][power.name] = list(power.homes) + state['influence'][power.name] = list(power.influence) + state['civil_disorder'][power.name] = power.civil_disorder + # Setting build + state['builds'][power.name] = {} + if self.phase_type != 'A': + state['builds'][power.name]['count'] = 0 + else: + state['builds'][power.name]['count'] = len(power.centers) - len(power.units) + state['builds'][power.name]['homes'] = [] + if state['builds'][power.name].get('count', 0) > 0: + build_sites = self._build_sites(power) + state['builds'][power.name]['count'] = min(len(build_sites), state['builds'][power.name]['count']) + state['builds'][power.name]['homes'] = build_sites + + # Returning state + return state + + 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 + """ + if clear_history: + self._clear_history() + + if 'map' in state and self.map.name != state['map']: + raise RuntimeError('Inconsistent state map (state: %s, game: %s)' % (state['map'], self.map.name)) + if 'rules' in state: + self.rules = [] + for rule in state['rules']: + self.add_rule(rule) + + if 'note' in state: + self.note = state['note'] + if 'name' in state and state['name']: + self.set_current_phase(state['name']) + if 'units' in state: + for power_name, units in state['units'].items(): + self.set_units(power_name, units, reset=True) + if 'centers' in state: + for power_name, centers in state['centers'].items(): + self.set_centers(power_name, centers, reset=True) + for power in self.powers.values(): + if 'homes' in state and power.name in state['homes']: + power.homes = list(state['homes'][power.name]) + else: + power.homes = list(self.map.homes[power.name]) + if 'influence' in state: + for power_name, influence in state['influence'].items(): + power = self.get_power(power_name) + power.influence = deepcopy(influence) + if 'civil_disorder' in state: + for power_name, civil_disorder in state['civil_disorder'].items(): + power = self.get_power(power_name) + power.civil_disorder = civil_disorder + + # Rebuilding hash and returning + self.rebuild_hash() + self._build_list_possible_convoys() + + def get_all_possible_orders(self, loc=None): + """ Computes a list of all possible orders for a unit in a given location + :param loc: Optional. The location where to get a list of orders (must include coasts) + If not provided, returns a list of all possible orders for all locations + :return: A list of orders for the unit, if there is a unit at location, or a list of possible + orders for all locations if no locations are provided. + """ + # pylint: disable=too-many-branches + # No locations, building a dict with all locations + if not loc: + all_possible_orders = {} + for map_loc in self.map.locs: + map_loc = map_loc.upper() + all_possible_orders[map_loc] = self.get_all_possible_orders(map_loc) + return all_possible_orders + + def remove_duplicates(list_with_dup): + """ Shorthand functions to remove duplicates """ + seen = set() + return [item for item in list_with_dup if not (item in seen or seen.add(item))] + + # Otherwise finding the possible orders at that specific location + possible_orders = [] + is_dislodged = False + unit = None + unit_power = None + + # If there are coasts possible, recursively adding the coasts first, then adding the loc orders + if '/' not in loc: + for loc_with_coast in [coast for coast in self.map.find_coasts(loc) if '/' in coast]: + possible_orders += self.get_all_possible_orders(loc_with_coast) + + # Determining if there is a unit at loc + # Dislodged unit have precedence over regular unit in Retreat phase + for power in self.powers.values(): + dislodged = [u for u in power.retreats if u[2:] == loc.upper()] + regular = [u for u in power.units if u[2:] == loc.upper()] + if dislodged: + is_dislodged = True + unit = dislodged[0] + unit_power = power + break + elif regular and not is_dislodged: + unit = regular[0] + unit_power = power + if self.phase_type != 'R': + break + + # No unit found, checking if location is a home + if unit is None: + if self.phase_type != 'A': + return remove_duplicates(possible_orders) + for power in self.powers.values(): + if loc[:3] in power.homes and 'BUILD_ANY' not in self.rules: + unit_power = power + break + if loc[:3] in power.centers and 'BUILD_ANY' in self.rules: + unit_power = power + break + + # Not a home, and no units + if not unit_power: + return remove_duplicates(possible_orders) + + # Determining if we can build or need to remove units + build_count = 0 if self.phase_type != 'A' else len(unit_power.centers) - len(unit_power.units) + + # Determining unit type and unit location + unit_type = unit[0] if unit else '' + unit_loc = unit[2:] if unit else '' + + # Movement phase + if self.phase_type == 'M': + # Computing coasts for dest + dest_1_hops = [l.upper() for l in self.map.abut_list(unit_loc, incl_no_coast=True)] + dest_with_coasts = [self.map.find_coasts(dest) for dest in dest_1_hops] + dest_with_coasts = {val for sublist in dest_with_coasts for val in sublist} + + # Hold + possible_orders += ['{} H'.format(unit)] + + # Move (Regular) and Support (Hold) + for dest in dest_with_coasts: + if self._abuts(unit_type, unit_loc, '-', dest): + possible_orders += ['{} - {}'.format(unit, dest)] + if self._abuts(unit_type, unit_loc, 'S', dest): + if self._unit_owner('A {}'.format(dest)): + possible_orders += ['{} S A {}'.format(unit, dest)] + elif self._unit_owner('F {}'.format(dest)): + possible_orders += ['{} S F {}'.format(unit, dest)] + + # Move Via Convoy + for dest in self._get_convoy_destinations(unit_type, unit_loc): + possible_orders += ['{} - {} VIA'.format(unit, dest)] + + # Support (Move) + for dest in dest_with_coasts: + + # Computing src of move (both from adjacent provinces and possible convoys) + # We can't support a unit that needs us to convoy it to its destination + abut_srcs = self.map.abut_list(dest, incl_no_coast=True) + convoy_srcs = self._get_convoy_destinations('A', dest, exclude_convoy_locs=[unit_loc]) + + # Computing coasts for source + src_with_coasts = [self.map.find_coasts(src) for src in abut_srcs + convoy_srcs] + src_with_coasts = {val for sublist in src_with_coasts for val in sublist} + + for src in src_with_coasts: + + # Checking if there is a unit on the src location + if self._unit_owner('A {}'.format(src)): + src_unit_type = 'A' + elif self._unit_owner('F {}'.format(src)): + src_unit_type = 'F' + else: + continue + + # Checking if src unit can move to dest (through adj or convoy), and that we can support it + # Only armies can move through convoy + if src[:3] != unit_loc[:3] \ + and self._abuts(unit_type, unit_loc, 'S', dest) \ + and ((src in convoy_srcs and src_unit_type == 'A') + or self._abuts(src_unit_type, src, '-', dest)): + + # Adding with coast + possible_orders += ['{} S {} {} - {}'.format(unit, src_unit_type, src, dest)] + + # Adding without coasts + if '/' in dest: + possible_orders += ['{} S {} {} - {}'.format(unit, src_unit_type, src, dest[:3])] + + # Convoy + if unit_type == 'F': + convoy_srcs = self._get_convoy_destinations(unit_type, unit_loc, unit_is_convoyer=True) + for src in convoy_srcs: + + # Checking if there is a unit on the src location + if unit_type == 'F' and self._unit_owner('A {}'.format(src)): + src_unit_type = 'A' + else: + continue + + # Checking where the src unit can actually go + convoy_dests = self._get_convoy_destinations(src_unit_type, src, unit_is_convoyer=False) + + # Adding them as possible moves + for dest in convoy_dests: + if self._has_convoy_path(src_unit_type, src, dest, convoying_loc=unit_loc): + possible_orders += ['{} C {} {} - {}'.format(unit, src_unit_type, src, dest)] + + # Retreat phase + if self.phase_type == 'R': + + # Disband + if is_dislodged: + possible_orders += ['{} D'.format(unit)] + + # Retreat + if is_dislodged: + retreat_locs = unit_power.retreats[unit] + for dest in retreat_locs: + dest = dest.upper() + if not self._unit_owner('A {}'.format(dest[:3]), coast_required=0) \ + and not self._unit_owner('F {}'.format(dest[:3]), coast_required=0): + possible_orders += ['{} R {}'.format(unit, dest)] + + # Adjustment Phase + if self.phase_type == 'A': + build_sites = self._build_sites(unit_power) + + # Disband + if build_count < 0 and unit: + possible_orders += ['{} D'.format(unit)] + + # Build Army / Fleet + if build_count > 0 \ + and loc[:3] in build_sites \ + and not self._unit_owner('A ' + loc[:3], coast_required=0) \ + and not self._unit_owner('F ' + loc[:3], coast_required=0): + if self.map.is_valid_unit('A {}'.format(loc)): + possible_orders += ['A {} B'.format(loc)] + if self.map.is_valid_unit('F {}'.format(loc)): + possible_orders += ['F {} B'.format(loc)] + + # Waive + if build_count > 0: + possible_orders += ['WAIVE'] + + # Removing duplicate + return remove_duplicates(possible_orders) + + # ==================================================================== + # Private Interface - CONVOYS Methods + # ==================================================================== + def _build_list_possible_convoys(self): + """ Regenerates the list of possible convoy paths given the current fleet locations """ + # Already generated + if self.convoy_paths_possible is not None: + return + self.convoy_paths_possible = [] + self.convoy_paths_dest = {} + + # Finding fleets on water + convoying_locs = [] + for power in self.powers.values(): + for unit in power.units: + if unit[0] == 'F' and self.map.area_type(unit[2:]) in ['WATER', 'PORT']: + convoying_locs += [unit[2:]] + convoying_locs = set(convoying_locs) + + # Finding all possible convoy paths + for nb_fleets in range(1, len(convoying_locs) + 1): + for start, fleets, dests in self.map.convoy_paths[nb_fleets]: + if fleets.issubset(convoying_locs): + self.convoy_paths_possible += [(start, fleets, dests)] + + # Marking path to dest + self.convoy_paths_dest.setdefault(start, {}) + for dest in dests: + self.convoy_paths_dest[start].setdefault(dest, []) + self.convoy_paths_dest[start][dest] += [fleets] + + 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 + """ + # Armies can't convoy fleet, so if unit being convoyed is not an army, convoy not possible + if not army: + return False + + # Army can convoy through water, all units can convoy through port + area_type = self.map.area_type(loc) + area_type_cond = ((area_type == 'WATER') == army or area_type == 'PORT') + + # Making sure there is a valid unit on thru location to perform convoy + unit_type_cond = self._unit_owner('F %s' % loc, coast_required=0) + return area_type_cond and unit_type_cond + + 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 + """ + # Not moving or no paths + if unit not in self.command or self.command[unit][0] != '-': + return False + if unit not in self.convoy_paths or not self.convoy_paths[unit]: + return False + + # Otherwise, convoying since there is still an active valid path + return True + + 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') + :param convoying_loc: Optional. If set, the convoying location must be in one of the paths + :return: A boolean flag to indicate if the convoy is possible (if all units cooperate) + """ + if unit != 'A': + return False + + # Checking in table if there is a valid path and optionally if the convoying loc is in the path + self._build_list_possible_convoys() + active_paths = self.convoy_paths_dest.get(start, {}).get(end, []) + return active_paths and (convoying_loc is None or [1 for path in active_paths if convoying_loc in path]) + + 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') + :return: A list of convoying units (e.g. ['F NAO', 'F MAO']) having current orders to convoy path + """ + convoying_units = [] + army = unit != 'F' + expected_order = 'C %s %s - %s' % (unit, start[:3], end[:3]) + for unit_loc, unit_order in list(self.command.items()): + if unit_order == expected_order and self._is_convoyer(army, unit_loc[2:]): + convoying_units += [unit_loc] + return convoying_units + + 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 + :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 + """ + if unit == 'A' and unit_is_convoyer: + return [] + if unit == 'F' and not unit_is_convoyer: + return [] + + # Building cache + self._build_list_possible_convoys() + + # If we are moving via convoy, we just read the destinations from the table + if not unit_is_convoyer: + if not exclude_convoy_locs: + return list(self.convoy_paths_dest.get(start, {}).keys()) + + # We need to loop to make sure there is a path without the excluded convoyer + dests = [] + for dest, paths in self.convoy_paths_dest.get(start, {}).items(): + for path in paths: + if not [1 for excluded_loc in exclude_convoy_locs if excluded_loc in path]: + dests += [dest] + break + return dests + + # If we are convoying, we need to loop through the possible convoy paths + valid_dests = set([]) + for _, fleets, dests in self.convoy_paths_possible: + if start in fleets and (exclude_convoy_locs is None + or not [1 for excluded_loc in exclude_convoy_locs if excluded_loc in fleets]): + valid_dests |= dests + return list(valid_dests) + + 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 + :param convoying_units: The list of units who can convoy the unit + :return: A list of paths from start to end using convoying_units + """ + if unit_type != 'A' or not convoying_units: + return [] + + # Building cache and finding possible paths with convoying units + # Adding start and end location to every path + self._build_list_possible_convoys() + fleets = {loc[2:] for loc in convoying_units} + paths = [path for path in self.convoy_paths_dest.get(start, {}).get(end, set([])) if path.issubset(fleets)] + paths = [[start] + list(path) + [end] for path in paths] + paths.sort(key=len) + + # No paths found + if not paths: + return [] + + # We have intent to convoy, so we can use all paths + if via: + return paths + + # Assuming intent if end is not reachable from start (i.e. a convoy is required) + if not self._abuts(unit_type, start, 'S', end): + return paths + + # Otherwise, detecting if we intended to convoy + unit_owner = self._unit_owner('%s %s' % (unit_type, start), coast_required=0) + for convoyer in convoying_units: + convoy_owner = self._unit_owner(convoyer, coast_required=1) + + # We have intent if one of the power's fleet issued a convoyed order + # and there was a path using that fleet to move from start to end + if unit_owner == convoy_owner and \ + self._has_convoy_path(unit_type, start, end, convoying_loc=convoyer[2:]): + return paths + + # We could not detect intent + return [] + + 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) + :return: The minimum distance from unit to one of the homes + """ + visited = [] + if not homes: + return 99999 + + # Modified Djikstra + to_check = PriorityDict() + to_check[start] = 0 + while to_check: + distance, current = to_check.smallest() + del to_check[current] + + # Found smallest distance + if current[:3] in homes: + return distance + + # Marking visited + if current in visited: + continue + visited += [current] + + # Finding neighbors and updating distance + for loc in self.map.abut_list(current, incl_no_coast=True): + loc = loc.upper() + if loc in visited: + continue + + # Calculating distance for armies over LAND/WATER/COAST and for Fleet over WATER/COAST + if unit_type == 'A' or self._abuts(unit_type, current, '-', loc): + loc_distance = to_check[loc] if loc in to_check else 99999 + to_check[loc] = min(distance + 1, loc_distance) + + # Could not find destination + return 99999 + + # ==================================================================== + # Private Interface - ORDER Validation Methods + # ==================================================================== + 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 + """ + # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements + # No order + if not order: + return None + word = order.split() + owner = self._unit_owner(unit) + rules = self.rules + + # No order + if not word: + return None + + status = 1 if owner is not None else 0 + unit_type = unit[0] + unit_loc = unit[2:] + order_type = word[0] + + # Make sure the unit exists (or if the player is in a game in which he can't necessarily know) could exist. + # Also make sure any mentioned (supported or conveyed) unit could exists and could reach the listed destination + if not self.map.is_valid_unit(unit): + if report: + self.error.append(err.GAME_ORDER_TO_INVALID_UNIT % unit) + return None + + # Support / Convoy - 'S A/F XXX - YYY' + if order_type in ('S', 'C') and word[1:]: + if word[1] in ('A', 'F'): + alter, other = word[1:3] + else: + alter, other = '?', word[1] + + # Checks if A/F XXX is a valid unit for loc (source) + other = alter + ' ' + other + if not self.map.is_valid_unit(other, no_coast_ok=1): + if report: + self.error.append(err.GAME_ORDER_INCLUDES_INVALID_UNIT % other) + return None + + # S [A/F] XXX - YYY + # Checks if A/F YYY is a valid unit for loc (dest) + if len(word) == 5 - (alter == '?'): + other = alter + ' ' + word[-1] + if not self.map.is_valid_unit(other, no_coast_ok=1): + if report: + self.error.append(err.GAME_ORDER_INCLUDES_INVALID_DEST % other) + return None + + # Check if unit exists + # Status - 1 if unit has owner, 0 otherwise (Non-existent unit) + if not status: + if report: + self.error.append(err.GAME_ORDER_NON_EXISTENT_UNIT % unit) + return None + if power is not owner: + if report: + self.error.append(err.GAME_ORDER_TO_FOREIGN_UNIT % unit) + return None + + # Validate that anything in a SHUT location is only ordered to HOLD + if self.map.area_type(unit_loc) == 'SHUT' and order_type != 'H': + if report: + self.error.append(err.GAME_UNIT_MAY_ONLY_HOLD % unit) + return None + + # Validate support and convoy orders + # Triggers error if Army trying to convoys + if order_type == 'C' and (unit_type != 'F' or (self.map.area_type(unit_loc) not in ('WATER', 'PORT'))): + if report: + self.error.append(err.GAME_CONVOY_IMPROPER_UNIT % (unit, order)) + return None + + # ------------------------------------------------------------- + # SUPPORT OR CONVOY ORDER + if order_type in ('C', 'S'): + + # Add the unit type (or '?') if not specified. + # Note that the unit type is NOT added to the actual order -- just used during checking. + order_text = 'CONVOY' if order_type == 'C' else 'SUPPORT' + if len(word) > 1 and word[1] not in ('A', 'F'): + terrain = self.map.area_type(word[1]) + if order_type == 'C': + word[1:1] = ['AF'[unit_type == 'A']] # Convoying the opposite unit type A-F and F-A + elif terrain == 'WATER': + word[1:1] = ['F'] + elif terrain == 'LAND': + word[1:1] = ['A'] + elif terrain: # Other terrain, trying to determine if XXX exist + its_unit_type = [unit_type for unit_type in 'AF' if self._unit_owner(unit_type + ' ' + word[1])] + if its_unit_type: + word[1:1] = its_unit_type + else: + if report: + self.error.append(err.GAME_INVALID_ORDER_NON_EXISTENT_UNIT % (order_text, unit, order)) + return None + else: + if report: + self.error.append(err.GAME_INVALID_ORDER_RECIPIENT % (order_text, unit, order)) + return None + + # Make sure we have enough to work with + # e.g. syntax S A XXX - YYY or at least S XXX YYY + if len(word) < 3: + if report: + self.error.append(err.GAME_BAD_ORDER_SYNTAX % (order_text, unit, order)) + return None + + # Check that the recipient of the support or convoy exists + rcvr, dest = ' '.join(word[1:3]), word[2] + if not self._unit_owner(rcvr, 0): + if report: + self.error.append(err.GAME_ORDER_RECIPIENT_DOES_NOT_EXIST % (order_text, unit, order)) + return None + + # Check that the recipient is not the same unit as the supporter + if unit_loc == dest: + if report: + self.error.append(err.GAME_UNIT_CANT_SUPPORT_ITSELF % (unit, order)) + return None + + # Only units on coasts can be convoyed, or invalid units convoying + if order_type == 'C' \ + and (word[1] != 'AF'[unit_type == 'A'] or self.map.area_type(dest) not in ('COAST', 'PORT')): + if report: + self.error.append(err.GAME_UNIT_CANT_BE_CONVOYED % (unit, order)) + return None + + # Handle orders of the form C U xxx - xxx and S U xxx - xxx + if len(word) == 5: + if word[3] != '-': + if report: + self.error.append(err.GAME_BAD_ORDER_SYNTAX % (order_text, unit, order)) + return None + dest = word[4] + + # Coast is specified in the move, but ignored in the support and convoy order + # DATC 6.B.4 + if '/' in dest: + dest = dest[:dest.find('/')] + + # Making sure the dest is COAST,PORT and that the convoyed order can land there + if order_type == 'C': + if not (self.map.area_type(dest) in ('COAST', 'PORT') + and self.map.is_valid_unit(word[1] + ' ' + dest, unit[0] < 'F')): + if report: + self.error.append(err.GAME_BAD_CONVOY_DESTINATION % (unit, order)) + return None + + # Checking that support can reach destination... + elif (not self._abuts(word[1], word[2], order_type, dest) + and (rcvr[0] == 'F' + or not self._has_convoy_path(word[1], word[2][:3], dest))): + if report: + self.error.append(err.GAME_SUPPORTED_UNIT_CANT_REACH_DESTINATION % (unit, order)) + return None + + # Make sure that a convoy order was formatted as above + elif order_type == 'C': + if report: + self.error.append(err.GAME_IMPROPER_CONVOY_ORDER % (unit, order)) + return None + + # Make sure a support order was either as above or as S U xxx or as S U xxx H + elif len(word) != 3 and (len(word) != 4 or word[-1] != 'H'): + if report: + self.error.append(err.GAME_IMPROPER_SUPPORT_ORDER % (unit, order)) + return None + + # Make sure the support destination can be reached... + if order_type == 'S': + if not self._abuts(unit_type, unit_loc, order_type, dest): + if report: + self.error.append(err.GAME_UNIT_CANT_PROVIDE_SUPPORT_TO_DEST % (unit, order)) + return None + + # ... or that the fleet can perform the described convoy + elif not self._has_convoy_path(rcvr[0], rcvr[2:5], dest, convoying_loc=unit_loc): + if report: + self.error.append(err.GAME_IMPOSSIBLE_CONVOY_ORDER % (unit, order)) + return None + + # ------------------------------------------------------------- + # MOVE order + elif order_type == '-': + # Expected format '- xxx' or '- xxx - yyy - zzz' + if (len(word) & 1 and word[-1] != 'VIA') or (len(word[:-1]) & 1 and word[-1] == 'VIA'): + if report: + self.error.append(err.GAME_BAD_MOVE_ORDER % (unit, order)) + return None + + # Only a convoying army can give a path + if len(word) > 2 and unit_type != 'A' and self.map.area_type(unit_loc) not in ('COAST', 'PORT'): + if report: + self.error.append(err.GAME_UNIT_CANT_CONVOY % (unit, order)) + return None + + # Step through every "- xxx" in the order and ensure the unit can get there at every step + src = unit_loc + order_type = 'C-'[len(word) == 2 or (len(word) == 3 and word[-1] == 'VIA')] + visit = [] + + # Checking that unit is not returning back where it started ... + if word[-1] == unit_loc and order_type < 'C': + if report: + self.error.append(err.GAME_MOVING_UNIT_CANT_RETURN % (unit, order)) + return None + + # For a multi-step convoy + if order_type == 'C': + + # Checking that destination is a COAST or PORT ... + if self.map.area_type(word[-1]) not in ('COAST', 'PORT'): + if report: + self.error.append(err.GAME_CONVOYING_UNIT_MUST_REACH_COST % (unit, order)) + return None + + # Making sure that army is not having a specific coast as destination ... + if unit_type == 'A' and '/' in word[-1]: + if report: + self.error.append(err.GAME_ARMY_CANT_CONVOY_TO_COAST % (unit, order)) + return None + + # Making sure that the syntax is '- xxx - yyy - zzz' + offset = 1 if word[-1] == 'VIA' else 0 + if [1 for x in range(0, len(word) - offset, 2) if word[x] != '-']: + if report: + self.error.append(err.GAME_BAD_MOVE_ORDER % (unit, order)) + return None + + # For every location touched + ride = word[1:len(word) - offset:2] + for num, to_loc in enumerate(ride): + + # Checking that ride is not visited twice ... + if to_loc in visit and 'CONVOY_BACK' not in rules: + if report: + self.error.append(err.GAME_CONVOY_UNIT_USED_TWICE % (unit, order)) + return None + visit += [to_loc] + + # Making sure the last 2 locations touch, and that A/F can convoy through them + # pylint: disable=too-many-boolean-expressions + if (not self._abuts(unit_type, src, order_type, to_loc) + and not self._has_convoy_path(unit_type, unit_loc, to_loc) + and (len(word) == 2 + or unit_type == 'A' and (not self._abuts('F', to_loc, 'S', src)) + or (unit_type == 'F' + and (to_loc[:3].upper() not in + [abut[:3].upper() for abut in self.map.abut_list(src[:3])])))): + if report: + self.error.append(err.GAME_UNIT_CANT_MOVE_INTO_DEST % (unit, order)) + return None + + # If VIA flag set, make sure there is at least a possible path + if word[-1] == 'VIA' and not self._has_convoy_path(unit_type, unit_loc, to_loc): + if report: + self.error.append(err.GAME_UNIT_CANT_MOVE_VIA_CONVOY_INTO_DEST % (unit, order)) + return None + + # If we are at an intermediary location + if num < len(ride) - 1: + + # Trying to portage convoy fleet through water + # or trying to convoy army through LAND or COAST + if (unit_type == 'F' + and ((unit_type == 'A' and self.map.area_type(to_loc) not in ('WATER', 'PORT')) + or unit_type + self.map.area_type(to_loc) == 'FWATER')): + if report: + self.error.append(err.GAME_BAD_CONVOY_MOVE_ORDER % (unit, order)) + return None + + # Making sure there is a unit there to convoy ... + if not self._unit_owner('AF'[unit_type == 'A'] + ' ' + to_loc): + if report: + self.error.append(err.GAME_CONVOY_THROUGH_NON_EXISTENT_UNIT % (unit, order)) + return None + + # Portaging fleets must finish the turn on a coastal location listed in upper-case + elif (num + and unit_type == 'F' + and (to_loc not in self.map.loc_abut or self.map.area_type(to_loc) not in ('COAST', 'PORT'))): + if report: + self.error.append(err.GAME_IMPOSSIBLE_CONVOY % (unit, order)) + return None + src = to_loc + + # ------------------------------------------------------------- + # HOLD order + elif order_type == 'H': + if len(word) != 1: + if report: + self.error.append(err.GAME_INVALID_HOLD_ORDER % (unit, order)) + return None + + else: + if report: + self.error.append(err.GAME_UNRECOGNIZED_ORDER_TYPE % (unit, order)) + return None + + # All done + return status + + 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']). + :return: The compacted and expanded order (e.g. ['A', 'RUM', 'S', 'A', 'BUL']) + """ + if not word: + return word + + result = self.map.compact(' '.join(word)) + result = self.map.vet(self.map.rearrange(result), 1) + + # Removing errors (Negative values) + final, order = [], '' + for result_ix, (token, token_type) in enumerate(result): + if token_type < 1: + if token_type == -1 * POWER: + self.error.append(err.GAME_UNKNOWN_POWER % token) + continue + elif token_type == -1 * UNIT: + self.error.append(err.GAME_UNKNOWN_UNIT_TYPE % token) + continue + elif token_type == -1 * LOCATION: + self.error.append(err.GAME_UNKNOWN_LOCATION % token) + elif token_type == -1 * COAST: + token_without_coast = token.split('/')[0] + if token_without_coast in self.map.aliases.values(): + self.error.append(err.GAME_UNKNOWN_COAST % token) + result[result_ix] = token_without_coast, -1 * LOCATION + else: + self.error.append(err.GAME_UNKNOWN_LOCATION % token) + elif token_type == -1 * ORDER: + self.error.append(err.GAME_UNKNOWN_ORDER_TYPE % token) + continue + else: + self.error.append(err.GAME_UNRECOGNIZED_ORDER_DATA % token) + continue + token_type = -1 * token_type + + # Remove power names. Checking ownership of the unit might be better + if token_type == POWER: + continue + + # Remove the "H" from any order having the form "u xxx S xxx H" + # Otherwise storing order + elif token_type == ORDER: + if order == 'S' and token == 'H': + continue + order += token + + # Treat each move order the same. Eventually we'd want to distinguish between them + elif token_type == MOVE_SEP: + result[result_ix] = '-', token_type + order += '-' + + elif token_type == OTHER: + order = '' + + # Spot ambiguous place names and coasts in support and convoy orders + if 'NO_CHECK' in self.rules: + if token_type == LOCATION and token in self.map.unclear: + self.error.append(err.GAME_AMBIGUOUS_PLACE_NAME % token) + if token_type == COAST and token.split('/')[0] in self.map.unclear: + self.error.append(err.GAME_AMBIGUOUS_PLACE_NAME % token) + + final += [token] + + # Default any fleet move's coastal destination, then we're done + return self.map.default_coast(final) + + def _expand_coast(self, word): + """ Makes sure the correct coast is specified (if any) is specified. + 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']) + """ + if not word: + return word + + unit_type = word[0] + loc = word[1] + loc_without_coast = loc[:loc.find('/')] if '/' in loc else loc + + # For armies: Removing coast if specified + if unit_type == 'A': + if '/' in loc: + word[1] = loc_without_coast + if len(word) == 4 and '/' in word[3]: + word[3] = word[3][:word[3].find('/')] + + # For fleets: If there is a unit in the country, but not on the specified coast, we need to correct the coast + elif self._unit_owner('%s %s' % (unit_type, loc), coast_required=1) is None \ + and self._unit_owner('%s %s' % (unit_type, loc_without_coast), coast_required=0) is not None: + + # Finding the correct coast + for loc in [l for l in self.map.locs if l[:3] == loc_without_coast]: + if self._unit_owner('%s %s' % (word[0], loc), coast_required=1) is not None: + word[1] = loc + break + + # Removing cost if unit is supporting an army moving to coast + # F WES S A MAR - SPA/SC -> F WES S A MAR - SPA + if len(word) == 7 and '/' in word[-1] and word[2] == 'S' and word[3] == 'A': + dest = word[-1] + word[-1] = dest[:dest.find('/')] + + # Adjusting the coast if a fleet is supporting a move to the wrong coast + # F WES S F GAS - SPA/SC -> F WES S F GAS - SPA/NC + if len(word) == 7 and word[0] == 'F' and word[2] == 'S' and word[3] == 'F' and '/' in word[-1]: + word = word[:3] + self.map.default_coast(word[3:6] + [word[6][:3]]) + + # Returning with coasts fixed + return word + + 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']) + """ + # dependent is set when A/F is expected afterwards (so at start and after C/S) + # had_type indicates that A/F was found + word, dependent, had_type = [], 1, 0 + for token in item: + if not dependent: + dependent = token in 'CS' + elif token in 'AF': + had_type = 1 + elif token in ('RETREAT', 'DISBAND', 'BUILD', 'REMOVE'): + pass + else: + try: + # We have a location + # Try to find an active or retreating unit at current location + unit = [unit for power in self.powers.values() + for unit in (power.units, power.retreats.keys())[self.phase_type == 'R'] + if unit[2:].startswith(token)][0] + + # If A/F is missing, add it + if not had_type: + word += [unit[0]] + + # Trying to detect if coast is specified in retrieved unit location + # If yes, update the token, so it incorporates coastal information + if self.map.is_valid_unit(word[-1] + unit[1:]): + token = unit[2:] + except IndexError: + pass + dependent = had_type = 0 + + # Add token to final list + word += [token] + 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. + :return: Nothing + """ + # converting to unique format + orders = {} + for unit, order in self.orders.items(): + orders[unit] = order + + # Add coasts to support and (portage) convoy orders for fleets moving to a specific coast + for unit, order in orders.items(): + # Only rewriting 'S F XXX - YYY' and 'C F XXX - YYY' + if order[:3] not in ('S F', 'C F'): + continue + word = order.split() + + # rcvr is the unit receiving the support or convoy (e.g. F XXX in S F XXX - BER) + # Making sure rcvr has also submitted orders (e.g. F XXX - YYY) + rcvr = ' '.join(word[1:3]) + try: + rcvr = [x for x in orders if x.startswith(rcvr)][0] + except IndexError: + # No orders found + continue + + # Updating order to include rcvr full starting position (with coasts) + orders[unit] = ' '.join([order[0], rcvr] + word[3:]).strip() + + # Checking if coast is specified in destination position + if '-' in order: + # his -> '- dest/coast' + # updating order if coast is specified in his dest, but not ours + his = ' '.join(orders.get(rcvr, '').split()[-2:]) + if his[0] == '-' and his.split('/')[0] == ' '.join(word[3:]): + orders[unit] = order[:2] + rcvr + ' ' + his + + # Updating game.orders object + self.orders[unit] = orders[unit] + + def _validate_status(self, reinit_powers=True): + """ Validates the status of the game object""" + # Loading map and setting victory condition + if not self.map: + self.load_map(reinit_powers=reinit_powers) + self.victory = self.map.victory + + # By default, 50% +1 of the scs + # Or for victory homes, half the average number of home centers belonging to other powers plus one + if not self.victory: + self.victory = [len(self.map.scs) // 2 + 1] + + # Ensure game phase was set + if not self.phase: + self.phase = self.map.phase + apart = self.phase.split() + if len(apart) == 3: + if '%s %s' % (apart[0], apart[2]) not in self.map.seq: + self.error += [err.GAME_BAD_PHASE_NOT_IN_FLOW] + self.phase_type = apart[2][0] + else: + self.phase_type = '-' + + # Validate the BEGIN phase (if one was given) + if self.phase == 'FORMING': + apart = self.map.phase.split() + try: + int(apart[1]) + del apart[1] + if ' '.join(apart) not in self.map.seq: + raise Exception() + except ValueError: + self.error += [err.GAME_BAD_BEGIN_PHASE] + + # Set victory condition + if self.phase not in ('FORMING', 'COMPLETED'): + try: + year = abs(int(self.phase.split()[1]) - self.map.first_year) + win = self.victory[:] + self.win = win[min(year, len(win) - 1)] + except ValueError: + self.error += [err.GAME_BAD_YEAR_GAME_PHASE] + + # Initialize power data + for power in self.powers.values(): + + # Initialize homes if needed + if power.homes is None: + power.homes = [] + for home in self.map.homes.get(power.name, []): + self.update_hash(power.name, loc=home, is_home=True) + power.homes.append(home) + + # ==================================================================== + # Private Interface - Generic methods + # ==================================================================== + 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'} + """ + if self.__class__.rule_cache: + return self.__class__.rule_cache + group = variant = '' + data, forced, denied = {}, {}, {} + file_path = os.path.join(settings.PACKAGE_DIR, 'README_RULES.txt') + + if not os.path.exists(file_path): + self.error.append(err.GAME_UNABLE_TO_FIND_RULES) + return data, forced, denied + + with open(file_path, 'r', encoding='utf-8') as file: + for line in file: + word = line.strip().split() + + # Rules are in the format <!-- RULE NAME !RULE_1 +RULE_2 --> + # Where ! indicates a denied rule, and + indicates a forced rule + if word[:2] == ['<!--', 'RULE'] and word[-1][-1] == '>': + + # <!-- RULE GROUP 6 Secrecy --> + # group would be '6 Secrecy' + if word[2] == 'GROUP': + group = ' '.join(word[3:-1]) + + # <!-- RULE VARIANT standard --> + elif word[2] == 'VARIANT': + variant = word[3] + forced[variant] = [x[1:] for x in word[4:-1] if x[0] == '+'] + denied[variant] = [x[1:] for x in word[4:-1] if x[0] == '!'] + + # <!-- RULE NAME !RULE_1 +RULE_2 --> + elif word[2] != 'END': + rule = word[2] + if rule not in data: + data[rule] = {'group': group, 'variant': variant} + for control in word[3:-1]: + if control[0] in '-=+!': + data[rule].setdefault(control[0], []).append(control[1:]) + + self.__class__.rule_cache = (data, forced, denied) + return data, forced, denied + + def _build_hash_table(self): + """ Builds the Zobrist hash tables """ + if not self.map or self.map_name in self.__class__.zobrist_tables: + return + + # Finding powers and locations + map_powers = sorted([power_name for power_name in self.map.powers]) + map_locs = sorted([loc.upper() for loc in self.map.locs if self.map.area_type(loc) != 'SHUT']) + nb_powers = len(map_powers) + nb_locs = len(map_locs) + + # Generating a standardized seed + np_state = np.random.get_state() + np.random.seed(12345 + sum([ord(x) * 7 ** ix for ix, x in enumerate(self.map_name)]) % 2 ** 32) + self.__class__.zobrist_tables[self.map_name] = { + 'unit_type': np.random.randint(1, sys.maxsize, [2, nb_locs]), + 'units': np.random.randint(1, sys.maxsize, [nb_powers, nb_locs]), + 'dis_unit_type': np.random.randint(1, sys.maxsize, [2, nb_locs]), + 'dis_units': np.random.randint(1, sys.maxsize, [nb_powers, nb_locs]), + 'centers': np.random.randint(1, sys.maxsize, [nb_powers, nb_locs]), + 'homes': np.random.randint(1, sys.maxsize, [nb_powers, nb_locs]), + 'map_powers': map_powers, + 'map_locs': map_locs + } + np.random.set_state(np_state) + + # ==================================================================== + # Private Interface - PROCESSING and phase change methods + # ==================================================================== + def _begin(self): + """ Called to begin the game and move to the start phase + :return: Nothing + """ + self._move_to_start_phase() + self.note = '' + self.win = self.victory[0] + # Create dummy power objects for non-loaded powers. + for power_name in self.map.powers: + if power_name not in self.powers: + self.powers[power_name] = Power(self, power_name, role=self.role) + # Initialize all powers. + for starter in self.powers.values(): + # Starter having type won't be initialized. + starter.initialize(self) + + def _process(self): + """ Processes the current phase of the game """ + # Convert all raw movement phase "ORDER"s in a NO_CHECK game to standard orders before calling + # Game.process(). All "INVALID" and "REORDER" orders are left raw -- the Game.move_results() method + # knows how to detect and report them + if 'NO_CHECK' in self.rules and self.phase_type == 'M': + for power in self.powers.values(): + orders, power.orders, civil_disorder = power.orders, {}, power.civil_disorder + for status, order in orders.items(): + if status[:5] != 'ORDER': + power.orders[status] = order + elif order: + self._add_order(power, order.split()) + power.civil_disorder = civil_disorder + + # Processing the game + if self.phase_type == 'M': + self._determine_orders() + self._add_coasts() + + # Resolving orders + self._resolve() + + 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 + """ + + # Save results for current phase. + # NB: result_history is updated here, neither in process() nor in draw(), + # unlike order_history, message_history and state_history. + self.result_history.put(self._phase_wrapper_type(self.current_short_phase), self.result) + self.result = {} + + # For each possible phase + for _ in self.map.seq: + + # If game is not yet started, or completed can't advance + if self.phase in (None, 'FORMING', 'COMPLETED'): + break + + # Finding next phase and setting variables + self.phase = self._find_next_phase() + self.phase_type = self.phase.split()[-1][0] + + # Check phase determines if we need to process phase (0) or can skip it (1) + if not self._check_phase(): + break + else: + raise Exception("FailedToAdvancePhase") + + # Rebuilding the convoy cache + self._build_list_possible_convoys() + + # Returning + return [] + + 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 + self.phase = self.map.phase + self.phase_type = self.phase.split()[-1][0] + + 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.) + :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) + """ + return self.map.find_next_phase(self.phase, phase_type, skip) + + 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.) + :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) + """ + return self.map.find_previous_phase(self.phase, phase_type, skip) + + def _get_start_phase(self): + """ Returns the name of the start phase""" + cur_phase, cur_phase_type = self.phase, self.phase_type + self._move_to_start_phase() + phase = self.phase + self.phase, self.phase_type = cur_phase, cur_phase_type + return phase + + 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 + # Beginning / End of game - Can't skip + if self.phase in (None, 'FORMING', 'COMPLETED'): + return 0 + + # When changing phases, clearing all caches + self.clear_cache() + + # Movement phase - Always need to process + if self.phase_type == 'M': + return 0 + + # Retreats phase + if self.phase_type == 'R': + # We need to process if there are retreats + if [1 for x in self.powers.values() if x.retreats]: + return 0 + + # Otherwise, clearing flags and skipping phase + for power in self.powers.values(): + for dis_unit in power.retreats: + self.update_hash(power.name, unit_type=dis_unit[0], loc=dis_unit[2:], is_dislodged=True) + power.retreats, power.adjust, power.civil_disorder = {}, [], 0 + self.result = {} + if 'DONT_SKIP_PHASES' in self.rules: + return 0 + return 1 + + # Adjustments phase + if self.phase_type == 'A': + # Capturing supply centers + self._capture_centers() + + # If completed, can't skip + if self.phase == 'COMPLETED': + return 0 + + # If we have units to remove or to build, we need to process + for power in self.powers.values(): + units, centers = len(power.units), len(power.centers) + if [x for x in power.centers if x in power.homes]: + centers += (0 + min(0, len([0 for x in power.units if x[2:5] in power.homes]))) + if units > centers or (units < centers and self._build_limit(power)): + return 0 + + # Otherwise, skipping + self.result = {} + if 'DONT_SKIP_PHASES' in self.rules: + return 0 + return 1 + + # Other phases. We need to process manually. + return 0 + + def _post_move_update(self): + """ Deletes orders and removes CD flag after moves """ + for power in self.powers.values(): + power.orders, power.civil_disorder = {}, 0 + + 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 + """ + + # Retrieving the list of homes (build sites) for the power, and the list of active powers + homes = power.homes + + # Can build on any of his centers + # -- BUILD_ANY: Powers may build new units at any owned supply center, not simply at their home supply centers. + if 'BUILD_ANY' in self.rules: + homes = power.centers + + # Updating homes to only include homes if they are unoccupied, + homes = [h for h in homes if h in power.centers and h not in + [u[2:5] for p in self.powers.values() for u in p.units]] + return homes + + 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. + :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 + """ + # Computing build_sites if not provided + if sites is None: + sites = self._build_sites(power) + + # Returning number of sites + return len(sites) + + 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 = {} + + # Score is the number of supply centers owned + for power in self.powers.values(): + score[power] = len([sc for sc in power.centers]) + return score + + 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) + :return: Nothing + """ + victors, this_year = [], self._calculate_victory_score() + year_centers = [this_year[x] for x in self.powers.values()] + + # Determining win + for power in self.powers.values(): + centers = this_year[power] + + # 1) you must have enough centers to win + if (centers >= self.win + + # 2) and you must grow or, if "HOLD_WIN", must have had a win + and (centers > last_year[power], last_year[power] >= self.win)['HOLD_WIN' in self.rules] + + # 3) and you must be alone in the lead (not required in case of SHARED_VICTORY) + and ('SHARED_VICTORY' in self.rules + or (centers, year_centers.count(centers)) == (max(year_centers), 1))): + victors += [power] + + # We have a winner! + if victors: + self._finish([victor.name for victor in victors]) + + # DRAW if 100 years + elif int(self.phase.split()[1]) - self.map.first_year + 1 == 100: + self.draw() + + 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() + + # If no power owns centers, initialize them + if not [1 for x in self.powers.values() if x.centers]: + for power in self.powers.values(): + for center in power.centers: + self.update_hash(power.name, loc=center, is_center=True) + power.centers = [] + for center in power.homes: + self.update_hash(power.name, loc=center, is_center=True) + power.centers.append(center) + + # Remember the current center count for the various powers, for use in victory condition check, + # then go through and see if any centers have been taken over + unowned = self.map.scs[:] + for power in self.powers.values(): + for center in power.centers: + if center in unowned: + unowned.remove(center) + + # Keep track of scs lost + self.lost = {} + for power in list(self.powers.values()) + [None]: + # Centers before takover + if power: + centers = power.centers + else: + centers = unowned + + # For each center, check if we took ownership + for center in centers[:]: + for owner in self.powers.values(): + + # 1) If center is unowned, or 2) owned by someone else and that we have a unit on it + # Proceed with transfer, and record lost + if (not power or owner is not power) and center in [x[2:5] for x in owner.units]: + self._transfer_center(power, owner, center) + if not power: + unowned.remove(center) + else: + self.lost[center] = power + break + + # Determining if we have a winner + self._determine_win(victory_score_prev_year) + + 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') + :return: Nothing + """ + if from_power: + self.update_hash(from_power.name, loc=center, is_center=True) + from_power.centers.remove(center) + if center not in to_power.centers: + self.update_hash(to_power.name, loc=center, is_center=True) + to_power.centers += [center] + + 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 + """ + # Setting outcome, and end date. Clearing orders and saving. + self.outcome = [self._phase_abbr()] + victors + self.note = 'Victory by: ' + ', '.join([vic[:3] for vic in victors]) + self.phase = 'COMPLETED' + self.set_status(strings.COMPLETED) + for power in self.powers.values(): + for dis_unit in power.retreats: + self.update_hash(power.name, unit_type=dis_unit[0], loc=dis_unit[2:], is_dislodged=True) + power.retreats, power.adjust, power.civil_disorder = {}, [], 0 + + 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 + """ + return self.map.phase_abbr(phase or self.phase) + + # ==================================================================== + # Private Interface - ORDER Submission methods + # ==================================================================== + def _add_order(self, power, word, expand=True, replace=True): + """ 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 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 + """ + if not word: + return None + + raw_word = word + + if expand: + # Check that the order is valid. If not, self.error will say why. + word = self._expand_order(word) + word = self._expand_coast(word) + word = self._add_unit_types(word) + word = self.map.default_coast(word) + + # Last word is '?' - Removing it + if word and len(word[-1]) == 1 and not word[-1].isalpha(): + word = word[:-1] + if len(word) < 2: + return self.error.append(err.STD_GAME_BAD_ORDER % ' '.join(word)) + + # Checking if we can order unit + unit, order = ' '.join(word[:2]), ' '.join(word[2:]) + owner = self._unit_owner(unit) + if not owner or owner is not power: + self.error += [err.STD_GAME_UNORDERABLE_UNIT % ' '.join(word)] + + # Validating order + elif order: + valid = self._valid_order(power, unit, order) + + # Valid order. But is it to a unit already ordered? This is okay in a NO_CHECK game, and + # we HOLD the unit. If not, pack it back into the power's order list. + if valid is not None: + power.civil_disorder = 0 + if valid == -1: + order += ' ?' + if unit not in power.orders or (replace and 'NO_CHECK' not in self.rules): + power.orders[unit] = order + elif 'NO_CHECK' in self.rules: + count = len(power.orders) + if power.orders[unit] not in ('H', order): + power.orders['REORDER %d' % count] = power.orders[unit] + count += 1 + power.orders[unit] = 'H' + power.orders['REORDER %d' % count] = ' '.join(word) + else: + self.error += [err.STD_GAME_UNIT_REORDERED % unit] + + # Invalid order in NO_CHECK game + elif 'NO_CHECK' in self.rules: + count = len(power.orders) + power.orders['INVALID %d' % count] = ' '.join(raw_word) + + # Returning nothing + return None + + 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', ...] + :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 + + 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 + """ + cur_power, had_orders, has_orders, powers = power, [], [], [] + + # For each order + for line in orders: + + word = line.strip().split() + who = cur_power + + if not word: + continue + + # Checking if the power can order + if not hasattr(who, 'orders'): + return self.error.append('%s HAS NO UNITS OF ITS OWN TO ORDER' % who.name) + + # NMR = No Moves Received (NMR or CLEAR command) + nmr = (len(word) == 1 + and word[0][word[0][:1] in '([':len(word[0]) - (word[0][-1:] in '])')].upper() in ('NMR', 'CLEAR')) + if who not in powers: + + # Empty orders before sticking any new orders in it. + had_orders += [who.orders] + powers += [who] + if nmr: + continue + + # If CLEAR or NMR, clear orders + elif nmr: + who.orders = {} + has_orders = [x for x in has_orders if x is not who] + continue + + # Adds orders + if 'NO_CHECK' in self.rules: + data = self._expand_order(word) + if len(data) < 3 and (len(data) == 1 or data[1] != 'H'): + self.error.append(err.STD_GAME_BAD_ORDER % line.upper()) + continue + + # Voiding previous order on same unit + if replace: + for order in who.orders: + order_parts = who.orders[order].split() + if len(order_parts) >= 2 and order_parts[1][:3] == word[1][:3]: + who.orders[order] = '' + + # Adding new order + who.orders['ORDER %d' % (len(who.orders) + 1)] = ' '.join(word) + else: + self._add_order(who, word, expand=expand, replace=replace) + if who.orders and who not in has_orders: + has_orders += [who] + + # Make sure the player can update his orders + if not powers: + return 1 + if self.error: + return self.error + + # Clear CD flag, even if orders were cleared + for who in powers: + who.civil_disorder = 0 + + # Returning nothing + return None + + 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]) + :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 + """ + # No orders, returning + if not orders: + power.adjust, power.civil_disorder = [], 0 + return + + # Processing each order + adjust, retreated = [], [] + for order in orders: + word = order.split() + if not word or len(word) < 2: + continue + + # Expanding and adding unit types + if expand: + word = self._expand_order([order]) + word = self._add_unit_types(word) + + # Add 'R' has order type for Retreat, 'D' for Disband + if word[0] == 'R' and len(word) > 3: + del word[0] + if word[0] in 'RD': + word = word[1:] + word[:1] + + # Checking if unit can retreat + unit = ' '.join(word[:2]) + try: + unit = [r_unit for r_unit in power.retreats if r_unit == unit or r_unit.startswith(unit + '/')][0] + except IndexError: + adjust += ['VOID ' + order] + self.error.append(err.GAME_UNIT_NOT_IN_RETREAT % unit) + continue + + # Checking if unit already retreated + if unit in retreated: + adjust += ['VOID ' + order] + self.error.append(err.GAME_TWO_ORDERS_FOR_RETREATING_UNIT % unit) + continue + word[1] = unit[2:] + + # Adding Disband for retreats with no destination + if len(word) == 3 and word[2] in 'RD': + word[2] = 'D' + + # Checking if retreat destination is valid + elif len(word) == 4 and word[2] in 'R-': + word[2] = 'R' + if word[3] not in power.retreats[unit]\ + or self._unit_owner('A {}'.format(word[3][:3]), coast_required=0) \ + or self._unit_owner('F {}'.format(word[3][:3]), coast_required=0): + self.error.append(err.GAME_INVALID_RETREAT_DEST % ' '.join(word)) + adjust += ['VOID ' + order] + continue + + # Invalid retreat order - Voiding + else: + self.error.append(err.GAME_BAD_RETREAT_ORDER % ' '.join(word)) + adjust += ['VOID ' + order] + continue + + # Adding retreat order and marking unit as retreated + retreated += [unit] + adjust += [' '.join(word)] + + # Replacing previous orders + if replace: + for order in adjust: + word = order.split() + if len(word) >= 2 and word[0] != 'VOID': + power.adjust = [adj_order for adj_order in power.adjust if adj_order.split()[1:2] != word[1:2]] + + # Otherwise, marking re-orders as invalid + else: + ordered_locs = [adj_order.split()[1] for adj_order in power.adjust] + for order in adjust[:]: + word = order.split() + if len(word) >= 2 and word[1] in ordered_locs: + self.error += [err.GAME_MULTIPLE_ORDERS_FOR_UNIT % ' '.join(word[:2])] + adjust.remove(order) + + # Finalizing orders + power.adjust += adjust + power.civil_disorder = 0 + + 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. + :param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders. + :return: List of processing errors + + 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 + """ + for who, adj in self._distribute_orders(power, orders): + self._add_retreat_orders(who, adj, expand=expand, replace=replace) + return self.error + + 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]) + :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 + """ + # pylint: disable=too-many-branches + # No orders submitted, returning + if not orders: + power.adjust, power.civil_disorder = [], 0 + return + + # Calculating if the power can build or remove units + adjust, places = [], [] + need, sites = len(power.centers) - len(power.units), [] + order_type = 'D' if need < 0 else 'B' + + # If we can build, calculating list of possible build locations + if need > 0: + sites = self._build_sites(power) + need = min(need, self._build_limit(power, sites)) + + # Processing each order + for order in orders: + order = order.strip() + + if order == 'WAIVE': + # Check WAIVE order immediately and continue to next loop step. + if need >= 0: + adjust += [order] + else: + adjust += ['VOID ' + order] + self.error += ['WAIVE NOT ALLOWED FOR DISBAND'] + continue + + if not order or len(order.split()) < 2: + continue + word = self._expand_order([order]) if expand else order.split() + + # Checking if unit can Build/Disband, otherwise voiding order + if word[-1] == order_type: + pass + elif word[-1] in 'BD': + adjust += ['VOID ' + order] + self.error += ['ORDER NOT ALLOWED: ' + order] + continue + + # Adding unit type + if word[-1] == 'D' and expand: + word = self._add_unit_types(word) + + # Checking for 'Disband' + order = ' '.join(word) + if word[-1] == 'D': + if len(word) == 3: + unit = ' '.join(word[:2]) + + # Invalid unit, voiding order + if unit not in power.units: + adjust += ['VOID ' + order] + self.error += [err.GAME_NO_SUCH_UNIT % unit] + + # Order to remove unit + elif order not in adjust: + adjust += [order] + + # Invalid order, voiding + else: + adjust += ['VOID ' + order] + self.error += [err.GAME_MULTIPLE_ORDERS_FOR_UNIT % unit] + else: + adjust += ['VOID ' + order] + self.error += [err.GAME_BAD_ADJUSTMENT_ORDER % order] + + # Checking for BUILD + elif len(word) == 3: + site = word[1][:3] + + # Invalid build site + if site not in sites: + adjust += ['VOID ' + order] + self.error += [err.GAME_INVALID_BUILD_SITE % order] + + # Site already used + elif site in places: + adjust += ['VOID ' + order] + self.error += [err.GAME_MULT_BUILDS_IN_SITE % order] + + # Unit can't be built there + elif not self.map.is_valid_unit(' '.join(word[:2])): + adjust += ['VOID ' + order] + self.error += [err.GAME_INVALID_BUILD_ORDER % order] + + # Valid build sites + else: + adjust += [order] + places += [site] + + # Otherwise, unknown order - Voiding + else: + adjust += ['VOID ' + order] + self.error += [err.GAME_BAD_ADJUSTMENT_ORDER % order] + + # NB: We skip WAIVE orders when checking for replacements. + # We will check them later. + + # Replacing previous orders + if replace: + for order in adjust: + word = order.split() + if len(word) >= 2 and word[0] != 'VOID': + power.adjust = [adj_order for adj_order in power.adjust + if adj_order == 'WAIVE' or adj_order.split()[1] != word[1]] + + # Otherwise, marking re-orders as invalid + else: + ordered_locs = [adj_order.split()[1] for adj_order in power.adjust if adj_order != 'WAIVE'] + for order in adjust[:]: + word = order.split() + if len(word) >= 2 and word[1] in ordered_locs: + self.error += [err.GAME_MULTIPLE_ORDERS_FOR_UNIT % ' '.join(word[:2])] + adjust.remove(order) + + # Finalizing orders + power.adjust += adjust + power.civil_disorder = 0 + + # We check WAIVE orders in power.adjust after updating power.adjust, + # as WAIVE orders depend on variable `need`, whom computation is relative to power + # (ie. not relative to orders being currently adjusted). + + # Removing extra waive orders + while 0 < need < len(power.adjust): + if 'WAIVE' in power.adjust: + power.adjust.remove('WAIVE') + else: + break + + # Adding missing waive orders + if 'WAIVE' in power.adjust or power.is_dummy(): + power.adjust.extend(['WAIVE'] * (need - len(power.adjust))) + + 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. + :param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders. + :return: List of processing errors + + 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 + """ + for who, adj in self._distribute_orders(power, orders): + self._add_adjust_orders(who, adj, expand=expand, replace=replace) + return self.error + + def _determine_orders(self): + """ Builds the self.orders dictionary (i.e. makes sure all orders are legitimate). """ + self.orders = {} + + # Determine the orders to be issued to each unit, based on unit ownership + for power in self.powers.values(): + for unit, order in power.orders.items(): + if power is self._unit_owner(unit): + self.orders[unit] = order + + # In NO_CHECK games, ensure that orders to other player's units are reported as invalid + # if no proxy was given + if 'NO_CHECK' in self.rules: + for power in self.powers.values(): + for unit, order in power.orders.items(): + if unit[0] not in 'RI' and power is not self._unit_owner(unit): + order = unit + ' ' + order + power.orders['INVALID %d' % len(power.orders)] = order + + def _default_orders(self, power): + """ Issues default orders for a power (HOLD) + :param power: The power instance + :return: Nothing + """ + # Power has no units + if not power.units: + return + + # Power has not submitted all his orders, checking if we default to HOLD + if not [x for x in power.units if self.orders.get(x)]: + power.civil_disorder = 1 + for unit in power.units: + self.orders.setdefault(unit, 'H') + + @classmethod + def _distribute_orders(cls, power, orders, clear=True): + """ For controlling powers, distribute orders to controlled powers + :param power: The power instance submitting the orders + :param orders: The list of orders submitted + :param clear: Boolean flag to indicate to clear order if NMR/CLEAR submitted + :return: A list of tuple with (controlled power instance, list of orders for that controlled power instance) + """ + powers, distributor = [power], {power.name: []} + cur_power = power + + # For each order submitted + for order in orders: + # Don't distribute blank order + word = order.strip().split() + if not word: + continue + + who = cur_power + + # Clearing orders for power + if (clear + and len(word) == 1 + and word[0][word[0][:1] in '([':len(word[0]) - (word[0][-1:] in '])')].upper() in ('NMR', 'CLEAR')): + distributor[who.name] = [] + # Otherwise, distributing order + else: + distributor[who.name] += [' '.join(word)] + + # Returning a list of tuples with the power instance and their respective orders. + return [(x, distributor[x.name]) for x in powers] + + # ==================================================================== + # Private Interface - ADJUDICATION Methods + # ==================================================================== + 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) + :param other_loc: The location of the other unit + :return: 1 if the locations are adjacent for the move, 0 otherwise + """ + # Check if the map says the adjacency is good + if not self.map.abuts(unit_type, unit_loc, order_type, other_loc): + return 0 + return 1 + + 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') + :return: The power instance who owns the unit or None + """ + # If coast_required is 0 and unit does not contain a '/' + # return the owner if we find a unit that starts with unit + # Don't count the unit if it needs to retreat (i.e. it has been dislodged) + for owner in self.powers.values(): + if unit in owner.units: + return owner + if not coast_required and '/' not in unit and [1 for x in owner.units if x.find(unit) == 0]: + return owner + return None + + 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 + """ + if any_coast: + site = site[:3] + for power in self.powers.values(): + for unit in power.units: + if unit[2:].startswith(site): + return unit + return None + + def _strengths(self): + """ 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: + # A MUN H, A SIL - MUN, A BOH S A SIL - MUN, A RUH - MUN would result in: + # 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) + self.combat = {} + + # For each order + for unit, order in self.command.items(): + word = order.split() + + # Strength of a non-move or failed move is 1 + support + if word[0] != '-' or self.result[unit]: + place, strength = unit[2:5], 1 + + # Strength of move depends on * and ~ in adjacency list + else: + offset = 1 if word[-1] == 'VIA' else 0 + place = word[-1 - offset][:3] + strength = 1 + + # Adds the list of supporting units + # Only adding the support that DOES NOT count toward dislodgment + self.combat \ + .setdefault(place, {}) \ + .setdefault(strength + self.supports[unit][0], []) \ + .append([unit, self.supports[unit][1]]) + + 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']) + :return: Boolean (1 or 0) to indicate if a paradox action was detected in the chain + """ + visited_units = [] + current_node = starting_node + current_unit = self._occupant(current_node) + while current_unit is not None and current_unit not in visited_units: + visited_units += [current_unit] + current_order = self.command.get(current_unit, 'H') + + # Action and last words detected + if (current_order[0] == paradox_action + and current_order.split()[-1 * len(paradox_last_words):] == paradox_last_words): + return True + + # Continuing chain only if order is Support or Convoy + if current_order.split()[0] not in 'SC': + break + current_node = current_order.split()[-1] + current_unit = self._occupant(current_node) + + # No paradox detected + return False + + 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']} + :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') + :return: Nothing + """ + for unit, word in may_convoy.items(): + + # Removing '-' + word = [w for w in word if w != '-'] + + # Checking order of unit at dest + offset = 1 if self.command.get(unit, []).split()[-1] == 'VIA' else 0 + convoy_dest = self.command.get(unit, 'H').split()[-1 - offset] + unit_at_dest = self._occupant(convoy_dest) + order_unit_at_dest = self.command.get(unit_at_dest, 'H') + + # Looping over all areas where convoys will take place (including destination) + for place in word: + area, convoyer = place[:3], 'AF'[unit[0] == 'A'] + ' ' + place + strongest = self.combat[area][max(self.combat[area])] + + # Checking if the convoy is under attack + for strong_unit in strongest: + if self._unit_owner(convoyer) != self._unit_owner(strong_unit[0]): + break + else: + continue + + # Paradox Detection #1 + # [1st and 2nd order] Checking that we are not attacking a chain, with the last unit supporting + # the convoy + paradox = self._detect_paradox(convoy_dest, 'S', ['S', 'F', area]) + + # Checking if the convoy can withstand the attack and there is not active paradox + if convoyer in [x[0] for x in strongest] and not paradox: + continue + + # For a beleaguered garrison, checking if the destination is attacking / supporting an attack + # against convoy + if len(strongest) >= 2 and not paradox: + if order_unit_at_dest.split()[0] not in '-S' or order_unit_at_dest.split()[-1][:3] != area: + continue + + # Removing paths using place + self.convoy_paths.setdefault(unit, []) + for path in self.convoy_paths[unit]: + if place in path: + self.convoy_paths[unit].remove(path) + + # Paradox Detection #2 - Can convoyed unit use land route to cut support necessary to attack convoy + paradox = False + if self._abuts(unit[0], unit[2:], '-', convoy_dest): + paradox = self._detect_paradox(convoy_dest, 'S', ['-', area]) + + # Setting the result if there is no convoy paths left, and + # 1) there is no land route (or there is a paradox through the land route) + # or 2) the unit specified 'VIA' and doesn't want to try the land route (4.A.3) + if not self.convoy_paths[unit] and (paradox + or not self._abuts(unit[0], unit[2:], '-', convoy_dest) + or (self._abuts(unit[0], unit[2:], '-', convoy_dest) + and self.command[unit].split()[-1] == 'VIA')): + self.result[unit] = [result] + + # Setting the result for a would-be dislodged fleet + if coresult: + self.result[convoyer] = [coresult] + + 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 + """ + self.result[unit] += ['bounce'] + self.combat \ + .setdefault(unit[2:5], {}) \ + .setdefault(1, []) \ + .append([unit, []]) + return 1 + + def _bounce(self): + """ This methods marks all units that can't get where they're going as bounced. + It loops to handle bounce-chains. + """ + # pylint: disable=too-many-nested-blocks + bounced = 1 + while bounced: + bounced = 0 + + # STEP 6. MARK (non-convoyed) PLACE-SWAP BOUNCERS + for unit, order in self.command.items(): + word = order.split() + if self.result[unit] or word[0] != '-' or self._is_moving_via_convoy(unit): + continue + crawl_ok, site = False, '- ' + unit[2:] + swap = self._occupant(word[1], any_coast=not crawl_ok) + if self._is_moving_via_convoy(swap): + continue + if not (crawl_ok and swap and swap[0] == unit[0] == 'F'): + site = site.split('/')[0] + if not (self.command.get(swap, '').find(site) or self.result[swap]): + my_strength = self.supports[unit][0] - len(self.supports[unit][1]) + his_strength = self.supports[swap][0] - len(self.supports[swap][1]) + our_strength = (self._unit_owner(unit) is self._unit_owner(swap) + or self.supports[unit][0] == self.supports[swap][0]) + if our_strength or my_strength <= his_strength: + self._boing(unit) + if our_strength or his_strength <= my_strength: + self._boing(swap) + + # Marking support used for self-dislodgement as void + for supporting_unit in self.supports[unit][1]: + self.result[supporting_unit] += ['void'] + for supporting_unit in self.supports[swap][1]: + self.result[supporting_unit] += ['void'] + bounced = 1 + if bounced: + continue + # No (more) swap-bouncers + + # STEP 7. MARK OUTGUNNED BOUNCERS + for place, conflicts in list(self.combat.items()): + strength = sorted(conflicts.keys()) + for key in strength: + if key != strength[-1] or len(conflicts[key]) != 1: + for unit, no_help in conflicts[key]: + if not self.result[unit] and self.command[unit][0] == '-': + bounced = self._boing(unit) + if bounced: + continue + # No (more) outgunned bouncers + + # STEP 8. MARK SELF-DISLODGE BOUNCERS + for place, conflicts in list(self.combat.items()): + strength = sorted(conflicts.keys()) + if len(conflicts[strength[-1]]) != 1: + continue + strongest = conflicts[strength[-1]][0][0] + if self.command[strongest][0] != '-' or self.result[strongest]: + continue + no_help = len(conflicts[strength[-1]][0][1]) + guy = self._occupant(place) + if guy: + owner = self._unit_owner(guy) + if ((self.command[guy][0] != '-' or self.result[guy]) + and (owner is self._unit_owner(strongest) + or (len(strength) > 1 and strength[-1] - no_help <= strength[-2]))): + bounced = self._boing(strongest) + for supporting_unit in conflicts[strength[-1]][0][1]: + if 'void' not in self.result[supporting_unit]: + self.result[supporting_unit] += ['void'] + + # No (more) self-dislodge bouncers + + 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 + """ + order = self.command[unit] + word = order.split() + if word[0] != '-' or (direct and self._is_moving_via_convoy(unit)): + return + dest = word[-1] if word[-1] != 'VIA' else word[-2] + other_unit = self._occupant(dest, any_coast=1) + coord = self.command.get(other_unit, 'no unit at dest').split() + support_target = 'F ' + coord[-1][:3] + + # pylint: disable=too-many-boolean-expressions + if (coord[0] == 'S' + and 'cut' not in self.result[other_unit] + and 'void' not in self.result[other_unit] + + # EXCEPTION A: CANNOT CUT SUPPORT YOU YOURSELF ARE GIVING + and (self._unit_owner(unit) is not self._unit_owner(other_unit)) + + # EXCEPTION B: CANNOT CUT SUPPORT FOR A MOVE AGAINST YOUR LOCATION + and coord[-1][:3] != unit[2:5] + + # EXCEPTION C: OR (IF CONVOYED) FOR OR AGAINST ANY CONVOYING FLEET + and (not self._is_moving_via_convoy(unit) + or self.command.get(support_target, 'H')[0] != 'C' + or 'void' in self.result.get(support_target, []) + # EXCEPTION TO EXCEPTION C: IF THERE IS A ALTERNATIVE CONVOY ROUTE + or [1 for path in self.convoy_paths[unit] if support_target[2:] not in path])): + + # Okay, the support is cut. + self.result[other_unit] += ['cut'] + affected = ' '.join(coord[1:3]) # Unit being supported + self.supports[affected][0] -= 1 + if other_unit in self.supports[affected][1]: + self.supports[affected][1].remove(other_unit) + + 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 + """ + sups = [strength for strength, attack_unit in self.combat[site].items() if unit in attack_unit][0] + self.combat[site][sups].remove(unit) + if not self.combat[site][sups]: + del self.combat[site][sups] + if not self.combat[site]: + del self.combat[site] + + 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 + """ + # Detecting if there is only one attack winning at site + most = max(self.combat[site]) + if len(self.combat[site][most]) > 1: + return None + + # Unbouncing the winner of the attack at site + unbouncer = self.combat[site][most][0][0] + if 'bounce' in self.result[unbouncer]: + self.result[unbouncer].remove('bounce') + if unbouncer in self.dislodged: + del self.dislodged[unbouncer] + return self.result[unbouncer].remove('dislodged') + + next_site = unbouncer[2:5] + self._no_effect([unbouncer, []], next_site) + if next_site in self.combat: + self._unbounce(next_site) + return None + + def _resolve_moves(self): + """ Resolves the list of orders """ + # pylint: disable=too-many-statements,too-many-branches + + # ----------------------------------------------------------- + # STEP 0: DECLARE ALL RESULTS AS YET UNKNOWN + self.result, self.supports, self.convoy_paths, may_convoy = {}, {}, {}, {} + + # Fill self.command from the self.orders dictionary + # Fill self.ordered_units from the powers.units list + # Default order is to hold + self.command = {} + self.ordered_units = {} + for power in self.powers.values(): + self.ordered_units[power.name] = [unit for unit in power.units if unit in self.orders] + for unit in power.units: + self.command[unit] = self.orders.get(unit, 'H') + if 'NO_CHECK' in self.rules: + for order in [order for key, order in power.orders.items() if key.startswith('INVALID')]: + unit = ' '.join(order.split()[:2]) + self.ordered_units[power.name] += [unit] + self.command[unit] = 'H' + self.result[unit] = ['void'] + self._default_orders(power) + + for unit in self.command: + self.result.setdefault(unit, []) + self.supports.setdefault(unit, [0, []]) + + # ----------------------------------------------------------- + # STEP 1A. CANCEL ALL INVALID ORDERS GIVEN TO UNITS ATTEMPTING TO MOVE BY CONVOY + for unit, order in list(self.command.items()): + word = order.split() + if word[0] != '-': + continue + + # Full convoy path has been specified (e.g. 'A PAR - MAR - NAO - MAO - LON') + offset = 1 if word[-1] == 'VIA' else 0 + if len(word) - offset > 2: + for convoyer in range(1, len(word) - 1, 2): + convoy_order = self.command.get('AF'[unit[0] == 'A'] + ' ' + word[convoyer]) + if convoy_order not in ['C %s - ' % x + word[-1] for x in (unit, unit[2:])]: + if convoy_order: + self.result[unit] += ['no convoy'] + else: + self.command[unit] = 'H' + break + # List the valid convoys + else: + may_convoy[unit] = order.split() + self.convoy_paths[unit] = [[unit[2:]] + word[1::2]] + + # Only src and dest provided + else: + def flatten(nested_list): + """ Flattens a sublist """ + return [list_item for sublist in nested_list for list_item in sublist] + + has_via_convoy_flag = 1 if word[-1] == 'VIA' else 0 + convoying_units = self._get_convoying_units_for_path(unit[0], unit[2:], word[1]) + possible_paths = self._get_convoy_paths(unit[0], + unit[2:], + word[1], + has_via_convoy_flag, + convoying_units) + + # No convoy path - Removing VIA and checking if adjacent + if not possible_paths: + if has_via_convoy_flag: + self.command[unit] = ' '.join(word[:-1]) + if not self._abuts(unit[0], unit[2:], 'S', word[1]): + self.result[unit] += ['no convoy'] + + # There is a convoy path, remembering the convoyers + else: + self.convoy_paths[unit] = possible_paths + may_convoy.setdefault(unit, []) + for convoyer in convoying_units: + if convoyer[2:] in flatten(possible_paths) and convoyer[2:] not in may_convoy[unit]: + may_convoy[unit] += [convoyer[2:]] + + # Marking all convoys that are not in any path + invalid_convoys = convoying_units[:] + all_path_locs = list(set(flatten(possible_paths))) + for convoy in convoying_units: + if convoy[2:] in all_path_locs: + invalid_convoys.remove(convoy) + for convoy in invalid_convoys: + self.result[convoy] = ['no convoy'] + + # ----------------------------------------------------------- + # STEP 1B. CANCEL ALL INVALID CONVOY ORDERS + for unit, order in self.command.items(): + if order[0] != 'C': + continue + # word = ['C', 'PAR', 'MAR'] -> ['C', 'A', 'PAR', 'MAR'] + word, mover_type = order.split(), 'AF'[unit[0] == 'A'] + if word[1] != mover_type: + word[1:1] = [mover_type] + mover = '%s %s' % (mover_type, word[2]) + if self._unit_owner(mover): + convoyer = may_convoy.get(mover, []) + offset = 1 if self.command.get(mover, '').split()[-1] == 'VIA' else 0 + mover_dest = self.command.get(mover, '').split()[-1 - offset] + if unit[2:] not in convoyer or word[-1] != mover_dest: + self.result[unit] += ['void'] + else: + self.command[unit] = 'H' + + # ----------------------------------------------------------- + # STEP 2. CANCEL INCONSISTENT SUPPORT ORDERS AND COUNT OTHERS + for unit, order in self.command.items(): + if order[0] != 'S': + continue + word, signal = order.split(), 0 + + # Remove any trailing "H" from a support-in-place order. + if word[-1] == 'H': + del word[-1] + self.command[unit] = ' '.join(word) + + # Stick the proper unit type (A or F) into the order; + # All supports will have it from here on + where = 1 + (word[1] in 'AF') + guy = self._occupant(word[where]) + + # See if there is a unit to receive the support + if not guy: + self.command[unit] = 'H' + if not signal: + self.result[unit] += ['void'] + continue + word[1:where + 1] = guy.split() + self.command[unit] = ' '.join(word) + + # See if the unit's order matches the supported order + if signal: + continue + coord = self.command[guy].split() + + # 1) Void if support is for hold and guy is moving + # 2) Void if support is for move and guy isn't going where support is given + # 3) Void if support is give, but move over convoy failed + offset = 1 if coord[-1] == 'VIA' else 0 + if ((len(word) < 5 and coord[0] == '-') + or (len(word) > 4 and (coord[0], coord[-1 - offset]) != ('-', word[4])) + or 'no convoy' in self.result[guy]): + self.result[unit] += ['void'] + continue + + # Okay, the support is valid + self.supports[guy][0] += 1 + + # If the unit is owned by the owner of the piece being attacked, add the unit to those + # whose supports are not counted toward dislodgment. + if coord[0] != '-': + continue + owner = self._unit_owner(unit) + other = self._unit_owner(self._occupant(coord[-1], any_coast=1)) + if owner is other: + self.supports[guy][1] += [unit] + + # ----------------------------------------------------------- + # STEP 3. LET DIRECT (NON-CONVOYED) ATTACKS CUT SUPPORTS + for unit in self.command: + if not self.result[unit]: + self._cut_support(unit, direct=1) + + # ----------------------------------------------------------- + # STEPS 4 AND 5. DETERMINE CONVOY DISRUPTIONS + cut, cutters = 1, [] + while cut: + cut = 0 + self._strengths() + + # STEP 4. CUT SUPPORTS MADE BY (non-maybe) CONVOYED ATTACKS + self._check_disruptions(may_convoy, 'maybe') + for unit in may_convoy: + if self.result[unit] or unit in cutters: + continue + self._cut_support(unit) + cutters += [unit] + cut = 1 + if cut: + continue + + # STEP 5. LOCATE NOW-DEFINITE CONVOY DISRUPTIONS, VOID SUPPORTS + # THESE CONVOYERS WERE GIVEN, AND ALLOW CONVOYING UNITS TO CUT SUPPORT + self._check_disruptions(may_convoy, 'no convoy', 'disrupted') + for unit in may_convoy: + if 'no convoy' in self.result[unit]: + for sup, help_unit in self.command.items(): + if not (help_unit.find('S %s' % unit) or self.result[sup]): + self.result[sup] = ['no convoy'] + if not (help_unit.find('C %s' % unit) or self.result[sup]): + self.result[sup] = ['no convoy'] + self.supports[unit] = [0, []] + elif 'maybe' in self.result[unit] and unit not in cutters: + self.result[unit], cut = [], 1 + self._cut_support(unit) + cutters += [unit] + + # Recalculate strengths now that some are reduced by cuts + self._strengths() + + # Mark bounces, then dislodges, and if any dislodges caused a cut + # loop over this whole kaboodle again + self.dislodged, cut = {}, 1 + while cut: # pylint: disable=too-many-nested-blocks + # ----------------------------------------------------------- + # STEPS 6-8. MARK BOUNCERS + self._bounce() + + # STEP 9. MARK SUPPORTS CUT BY DISLODGES + cut = 0 + for unit, order in self.command.items(): + if order[0] != '-' or self.result[unit]: + continue + attack_order = order.split() + offset = 1 if attack_order[-1] == 'VIA' else 0 + victim = self._occupant(attack_order[-1 - offset], any_coast=1) + if victim and self.command[victim][0] == 'S' and not self.result[victim]: + word = self.command[victim].split() + supported, sup_site = self._occupant(word[2]), word[-1][:3] + + # This next line is the key. Convoyed attacks can dislodge, but even when doing so, they cannot cut + # supports offered for or against a convoying fleet + # (They can cut supports directed against the original position of the army, though.) + if len(attack_order) > 2 and sup_site != unit[2:5]: + continue + self.result[victim] += ['cut'] + cut = 1 + for sups in self.combat.get(sup_site, {}): + for guy, no_help in self.combat[sup_site][sups]: + if guy != supported: + continue + self.combat[sup_site][sups].remove([guy, no_help]) + if not self.combat[sup_site][sups]: + del self.combat[sup_site][sups] + sups -= 1 + if victim in no_help: + no_help.remove(victim) + self.combat[sup_site].setdefault(sups, []).append([guy, no_help]) + break + else: + continue + break + + # ----------------------------------------------------------- + # STEP 10. MARK DISLODGEMENTS AND UNBOUNCE ALL MOVES THAT LEAD TO DISLODGING UNITS + for unit, order in self.command.items(): + if order[0] != '-' or self.result[unit]: + continue + site = unit[2:5] + offset = 1 if order.split()[-1] == 'VIA' else 0 + loser = self._occupant(order.split()[-1 - offset], any_coast=1) + if loser and (self.command[loser][0] != '-' or self.result[loser]): + self.result[loser] = [res for res in self.result[loser] if res != 'disrupted'] + ['dislodged'] + self.dislodged[loser] = site + + # Check for a dislodged swapper (attacker and dislodged units must not be convoyed.) + # If found, remove the swapper from the combat list of the attacker's space + head_to_head_battle = not self._is_moving_via_convoy(unit) and not self._is_moving_via_convoy(loser) + if self.command[loser][2:5] == site and head_to_head_battle: + for sups, items in self.combat.get(site, {}).items(): + item = [x for x in items if x[0] == loser] + if item: + self._no_effect(item[0], site) + break + + # Marking support for self-dislodgement as void + for supporting_unit in self.supports[unit][1]: + self.result[supporting_unit] += ['void'] + + # Unbounce any powerful-enough move that can now take the spot being vacated by the dislodger. + if site in self.combat: + self._unbounce(site) + + # Done :-) + + 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 + self._resolve_moves() + + # Determine any retreats + for power in self.powers.values(): + for unit in [u for u in power.units if u in self.dislodged]: + if unit not in power.retreats: + self.update_hash(power.name, unit_type=unit[0], loc=unit[2:], is_dislodged=True) + power.retreats.setdefault(unit, []) + attacker_site, site = self.dislodged[unit], unit[2:] + attacker = self._occupant(attacker_site) + if self.map.loc_abut.get(site): + pushee = site + else: + pushee = site.lower() + for abut in self.map.loc_abut[pushee]: + abut = abut.upper() + where = abut[:3] + if ((self._abuts(unit[0], site, '-', abut) or self._abuts(unit[0], site, '-', where)) + and (not self.combat.get(where) + and where != attacker_site or self._is_moving_via_convoy(attacker))): + + # Armies cannot retreat to specific coasts + if unit[0] == 'F': + power.retreats[unit] += [abut] + elif where not in power.retreats[unit]: + power.retreats[unit] += [where] + + # List all possible retreats + destroyed, self.popped = {}, [] + if self.dislodged: + for power in self.powers.values(): + for unit in [u for u in power.units if u in self.dislodged]: + + # Removing unit + self.update_hash(power.name, unit_type=unit[0], loc=unit[2:]) + power.units.remove(unit) + to_where = power.retreats.get(unit) + + # Describing what it can do + if to_where: + pass + else: + destroyed[unit] = power + self.popped += [unit] + + # Now (finally) actually move the units that succeeded in moving + for power in self.powers.values(): + for unit in power.units[:]: + if self.command[unit][0] == '-' and not self.result[unit]: + offset = 1 if self.command[unit].split()[-1] == 'VIA' else 0 + + # Removing + self.update_hash(power.name, unit_type=unit[0], loc=unit[2:]) + power.units.remove(unit) + + # Adding + new_unit = unit[:2] + self.command[unit].split()[-1 - offset] + self.update_hash(power.name, unit_type=new_unit[0], loc=new_unit[2:]) + power.units += [new_unit] + + # Setting influence + for influence_power in self.powers.values(): + if new_unit[2:5] in influence_power.influence: + influence_power.influence.remove(new_unit[2:5]) + power.influence.append(new_unit[2:5]) + + # If units were destroyed, other units may go out of sight + if destroyed: + for unit, power in destroyed.items(): + if unit in power.retreats: + self.update_hash(power.name, unit_type=unit[0], loc=unit[2:], is_dislodged=True) + del power.retreats[unit] + + # All finished + self._post_move_update() + return [] + + 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 + self.command = {} + self.ordered_units = {} + conflicts = {} + + # Adjustments + if self.phase_type == 'A': + self.result = {} + + # Emptying the results for the Adjustments Phase + for power in self.powers.values(): + self.ordered_units.setdefault(power.name, []) + for order in power.adjust[:]: + + # Void order - Marking it as such in results + if order.split()[0] == 'VOID': + word = order.split()[1:] + unit = ' '.join(word[:2]) + self.result.setdefault(unit, []).append('void') + power.adjust.remove(order) + if unit not in self.ordered_units[power.name]: + self.ordered_units[power.name] += [unit] + + # Valid order - Marking as unprocessed + else: + word = order.split() + unit = ' '.join(word[:2]) + self.result.setdefault(unit, []) + if unit not in self.ordered_units[power.name]: + self.ordered_units[power.name] += [unit] + + # CIVIL DISORDER + for power in self.powers.values(): + diff = len(power.units) - len(power.centers) + + # Detecting missing orders + for order in power.adjust[:]: + if diff == 0: + word = order.split() + unit = ' '.join(word[:2]) + self.result.setdefault(unit, []).append('void') + power.adjust.remove(order) + + # Looking for builds + elif diff < 0: + word = order.split() + unit = ' '.join(word[:2]) + if word[-1] == 'B': + diff += 1 + else: + self.result.setdefault(unit, []).append('void') + power.adjust.remove(order) + + # Looking for removes + else: + word = order.split() + unit = ' '.join(word[:2]) + if word[-1] == 'D': + diff -= 1 + else: + self.result.setdefault(unit, []).append('void') + power.adjust.remove(order) + + if not diff: + continue + + power.civil_disorder = 1 + + # Need to remove units + if diff > 0: + fleets = PriorityDict() + armies = PriorityDict() + + # Calculating distance to home + for unit in power.units: + distance = self._get_distance_to_home(unit[0], unit[2:], power.homes) + if unit[0] == 'F': + fleets[unit] = -1 * distance + else: + armies[unit] = -1 * distance + + # Removing units + for unit in range(diff): + goner_distance, goner = 99999, None + + # Removing units with largest distance (using fleets if they are equal) + # (using alpha name if multiple units) + if fleets: + goner_distance, goner = fleets.smallest() + if armies and armies.smallest()[0] < goner_distance: + goner_distance, goner = armies.smallest() + if goner is None: + break + if goner[0] == 'F': + del fleets[goner] + else: + del armies[goner] + power.adjust += ['%s D' % goner] + self.result.setdefault(goner, []) + + # Need to build units + else: + sites = self._build_sites(power) + need = min(self._build_limit(power, sites), -diff) + power.adjust += ['WAIVE'] * need + + # Retreats phase + elif self.phase_type == 'R': + self.result = {} + + # Emptying the results for the Retreats Phase + for power in self.powers.values(): + self.ordered_units.setdefault(power.name, []) + for retreats in power.retreats: + self.result[retreats] = [] + + # Emptying void orders - And marking them as such + for power in self.powers.values(): + for order in power.adjust[:]: + if order.split()[0] == 'VOID': + word = order.split()[1:] + unit = ' '.join(word[:2]) + self.result[unit] = ['void'] + if unit not in self.ordered_units[power.name]: + self.ordered_units[power.name] += [unit] + power.adjust.remove(order) + + # Disband units with no retreats + for power in self.powers.values(): + if power.retreats and not power.adjust: + power.civil_disorder = 1 + power.adjust = ['%s D' % r_unit for r_unit in power.retreats] + + # Determine multiple retreats to the same location. + for power in self.powers.values(): + for order in power.adjust or []: + word = order.split() + if len(word) == 4: + conflicts.setdefault(word[3][:3], []).append(' '.join(word[:2])) + + # Determine retreat conflict (*bounce, destroyed*) + # When finished, "self.popped" will be a list of all retreaters who didn't make it. + for retreaters in conflicts.values(): + if len(retreaters) > 1: + for retreater in retreaters: + if 'void' in self.result[retreater]: + self.result[retreater].remove('void') + self.result[retreater] += ['bounce', 'disband'] + self.popped += retreaters + + # Processing Build and Disband + for power in self.powers.values(): + diff = len(power.units) - len(power.centers) + + # For each order + for order in power.adjust or []: + word = order.split() + unit = ' '.join(word[:2]) + + # Build + if word[-1] == 'B' and len(word) > 2: + if diff < 0: + self.update_hash(power.name, unit_type=unit[0], loc=unit[2:]) + power.units += [' '.join(word[:2])] + diff += 1 + self.result[unit] += [''] + else: + self.result[unit] += ['void'] + if unit not in self.ordered_units[power.name]: + self.ordered_units[power.name] += [unit] + + # Disband + elif word[-1] == 'D' and self.phase_type == 'A': + if diff > 0 and ' '.join(word[:2]) in power.units: + self.update_hash(power.name, unit_type=unit[0], loc=unit[2:]) + power.units.remove(' '.join(word[:2])) + diff -= 1 + self.result[unit] += [''] + else: + self.result[unit] += ['void'] + if unit not in self.ordered_units[power.name]: + self.ordered_units[power.name] += [unit] + + # Retreat + elif len(word) == 4: + if unit not in self.popped: + self.update_hash(power.name, unit_type=word[0], loc=word[-1]) + power.units += [word[0] + ' ' + word[-1]] + if unit in self.dislodged: + del self.dislodged[unit] + + # Update influence + for influence_power in self.powers.values(): + if word[-1] in influence_power.influence: + influence_power.influence.remove(word[-1]) + power.influence.append(word[-1]) + + if unit not in self.ordered_units[power.name]: + self.ordered_units[power.name] += [unit] + + for dis_unit in power.retreats: + self.update_hash(power.name, unit_type=dis_unit[0], loc=dis_unit[2:], is_dislodged=True) + power.adjust, power.retreats, power.civil_disorder = [], {}, 0 + + # Disbanding + for unit in [u for u in self.dislodged]: + self.result.setdefault(unit, []) + if 'disband' not in self.result[unit]: + self.result[unit] += ['disband'] + del self.dislodged[unit] + if unit not in self.popped: + self.popped += [unit] + + return [] + + 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 + + # This method knows how to process movement, retreat, and adjustment phases. + # For others, implement resolve_phase() + if this_phase == 'M': + self._move_results() + elif this_phase in 'RA': + self._other_results() + self._advance_phase() + + def _clear_history(self): + """ Clear all game history fields. """ + self.state_history.clear() + self.order_history.clear() + self.result_history.clear() + self.message_history.clear() + self.clear_orders() + self.clear_vote() diff --git a/diplomacy/engine/map.py b/diplomacy/engine/map.py new file mode 100644 index 0000000..677a4e7 --- /dev/null +++ b/diplomacy/engine/map.py @@ -0,0 +1,1361 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +# -*- coding: utf-8 -*- +# pylint: disable=too-many-lines +""" Map + - Contains the map object which represents a map where the game can be played +""" +from copy import deepcopy +import os +from diplomacy import settings +from diplomacy.utils import KEYWORDS, ALIASES +import diplomacy.utils.errors as err + +# Constants +UNDETERMINED, POWER, UNIT, LOCATION, COAST, ORDER, MOVE_SEP, OTHER = 0, 1, 2, 3, 4, 5, 6, 7 +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})]} + - 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 + e.g. 'standard' + - 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 + + __slots__ = ['name', 'first_year', 'victory', 'phase', 'validated', 'flow_sign', 'root_map', 'abuts_cache', + 'homes', 'loc_name', 'loc_type', 'loc_abut', 'loc_coasts', 'own_word', 'abbrev', 'centers', 'units', + 'pow_name', 'rules', 'files', 'powers', 'scs', 'owns', 'inhabits', 'flow', 'dummies', 'locs', 'error', + 'seq', 'phase_abbrev', 'unclear', 'unit_names', 'keywords', 'aliases', 'convoy_paths'] + + 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 + """ + if name in MAP_CACHE and use_cache: + return MAP_CACHE[name] + return object.__new__(cls) + + def __init__(self, name='standard', use_cache=True): + """ Constructor function + :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 + """ + if name in MAP_CACHE: + return + self.name = name + self.first_year = 1901 + self.victory = self.phase = self.validated = self.flow_sign = None + self.root_map = None + self.abuts_cache = {} + self.homes, self.loc_name, self.loc_type, self.loc_abut, self.loc_coasts = {}, {}, {}, {}, {} + self.own_word, self.abbrev, self.centers, self.units, self.pow_name = {}, {}, {}, {}, {} + self.rules, self.files, self.powers, self.scs, self.owns, self.inhabits = [], [], [], [], [], [] + self.flow, self.dummies, self.locs = [], [], [] + self.error, self.seq = [], [] + self.phase_abbrev, self.unclear = {}, {} + self.unit_names = {'A': 'ARMY', 'F': 'FLEET'} + self.keywords, self.aliases = KEYWORDS.copy(), ALIASES.copy() + self.load() + self.build_cache() + self.validate() + if name not in CONVOYS_PATH_CACHE and use_cache: + CONVOYS_PATH_CACHE[name] = add_to_cache(name) + self.convoy_paths = CONVOYS_PATH_CACHE.get(name, {}) + if use_cache: + MAP_CACHE[name] = self + + def __deepcopy__(self, memo): + """ Fast deep copy implementation """ + actual_init = self.__class__.__init__ + self.__class__.__init__ = lambda *args, **kwargs: None + instance = self.__class__(name=self.name, use_cache=False) + self.__class__.__init__ = actual_init + for key in self.__slots__: + setattr(instance, key, deepcopy(getattr(self, key))) + return instance + + def __str__(self): + return self.name + + 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 + """ + # pylint: disable=too-many-branches + # Already validated, returning (except if forced or if validating phases) + if not force and self.validated: + return + self.validated = 1 + + # Root map + self.root_map = self.root_map or self.name + + # Validating powers + self.powers = [power_name for power_name in self.homes if power_name != 'UNOWNED'] + self.powers.sort() + if len(self.powers) < 2: + self.error += [err.MAP_LEAST_TWO_POWERS] + + # Validating area type + for place in self.loc_name.values(): + if place.upper() not in self.powers and not self.area_type(place): + self.error += [err.MAP_LOC_NOT_FOUND % place] + + # Validating adjacencies + for place, abuts in self.loc_abut.items(): + up_abuts = [loc.upper() for loc in abuts] + for abut in abuts: + up_abut = abut.upper() + if up_abuts.count(up_abut) > 1: + self.error += [err.MAP_SITE_ABUTS_TWICE % (place.upper(), up_abut)] + while up_abut in up_abuts: + up_abuts.remove(up_abut) + + # Checking full name + if place.upper() not in self.loc_name.values(): + self.error += [err.MAP_NO_FULL_NAME % place] + + # Checking one-way adjacency + for loc in abuts: + if self.area_type(place) != 'SHUT' \ + and self.area_type(loc) != 'SHUT' \ + and not self.abuts('A', loc, '-', place) \ + and not self.abuts('F', loc, '-', place): + self.error.append(err.MAP_ONE_WAY_ADJ % (place, loc)) + + # Validating home centers + for power_name, places in self.homes.items(): + for site in places: + # Adding home as supply center + if site not in self.scs: + self.scs += [site] + if not self.area_type(site): + self.error += [err.MAP_BAD_HOME % (power_name, site)] + + # Remove home centers from unowned list. + # It's perfectly OK for 2 powers to share a home center, as long + # as no more than one owns it at the same time. + if power_name != 'UNOWNED': + if site in self.homes['UNOWNED']: + self.homes['UNOWNED'].remove(site) + + # Valid supply centers + for scs in self.centers.values(): + self.scs.extend([center for center in scs if center not in self.scs]) + + # Validating initial centers and units + for power_name, places in self.centers.items(): + for loc in places: + if not self.area_type(loc): + self.error.append(err.MAP_BAD_INITIAL_OWN_CENTER % (power_name, loc)) + + # Checking if power has OWN line + for power_name in self.powers: + if power_name not in self.owns: + self.centers[power_name] = self.homes[power_name][:] + for unit in self.units.get(power_name, []): + if not self.is_valid_unit(unit): + self.error.append(err.MAP_BAD_INITIAL_UNITS % (power_name, unit)) + + # Checking for multiple owners + for power_name, centers in self.centers.items(): + for site in centers: + for other, locs in self.centers.items(): + if other == power_name and locs.count(site) != 1: + self.error += [err.MAP_CENTER_MULT_OWNED % site] + elif other != power_name and locs.count(site) != 0: + self.error += [err.MAP_CENTER_MULT_OWNED % site] + if 'UNOWNED' in self.homes: + del self.homes['UNOWNED'] + + # Ensure a default game-year FLOW + self.flow = ['SPRING:MOVEMENT,RETREATS', 'FALL:MOVEMENT,RETREATS', 'WINTER:ADJUSTMENTS'] + self.flow_sign = 1 + self.seq = ['NEWYEAR', 'SPRING MOVEMENT', 'SPRING RETREATS', 'FALL MOVEMENT', 'FALL RETREATS', + 'WINTER ADJUSTMENTS'] + self.phase_abbrev = {'M': 'MOVEMENT', 'R': 'RETREATS', 'A': 'ADJUSTMENTS'} + + # Validating initial game phase + self.phase = self.phase or 'SPRING 1901 MOVEMENT' + phase = self.phase.split() + if len(phase) != 3: + self.error += [err.MAP_BAD_PHASE % self.phase] + else: + self.first_year = int(phase[1]) + + 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 + """ + # pylint: disable=too-many-nested-blocks,too-many-statements,too-many-branches + # If file_name is string, opening file from disk + # Otherwise file_name is the file handler + power = 0 + if file_name is None: + file_name = '{}.map'.format(self.name) + file_path = os.path.join(settings.PACKAGE_DIR, 'maps', file_name) + + # Checking if file exists: + found_map = 1 if os.path.exists(file_path) else 0 + if not found_map: + self.error.append(err.MAP_FILE_NOT_FOUND % file_name) + return + + # Adding to parsed files + self.files += [file_name] + + # Parsing file + with open(file_path, encoding='utf-8') as file: + variant = 0 + + for line in file: + word = line.split() + + # -- # comment... + if not word or word[0][0] == '#': + continue + upword = word[0].upper() + + # ---------------------------------- + # Centers needed to obtain a VICTORY + # -- VICTORY centerCount... + if upword == 'VICTORY': + try: + self.victory = [int(word) for word in word[1:]] + except ValueError: + self.error += [err.MAP_BAD_VICTORY_LINE] + + # --------------------------------- + # Inclusion of other base map files + # -- USE[S] fileName... + # -- MAP mapName + elif upword in ('USE', 'USES', 'MAP'): + if upword == 'MAP': + if len(word) != 2: + self.error += [err.MAP_BAD_ROOT_MAP_LINE] + elif self.root_map: + self.error += [err.MAP_TWO_ROOT_MAPS] + else: + self.root_map = word[1].split('.')[0] + for new_file in word[1:]: + if '.' not in new_file: + new_file = '{}.map'.format(new_file) + if new_file not in self.files: + self.load(new_file) + else: + self.error += [err.MAP_FILE_MULT_USED % new_file] + + # ------------------------------------ + # Set BEGIN phase + # -- BEGIN season year phaseType + elif upword == 'BEGIN': + self.phase = ' '.join(word[1:]).upper() + + # ------------------------------------ + # RULEs specific to this map + elif upword in ('RULE', 'RULES'): + if (variant or 'ALL') == 'ALL': + self.rules += line.upper().split()[1:] + + # ------------------------------------ + # -- [oldAbbrev ->] placeName = abbreviation alias... + elif '=' in line: + token = line.upper().split('=') + if len(token) == 1: + self.error += [err.MAP_BAD_ALIASES_IN_FILE % token[0]] + token += [''] + old_name, name, word = 0, token[0].strip(), token[1].split() + parts = [part.strip() for part in name.split('->')] + if len(parts) == 2: + old_name, name = parts + elif len(parts) > 2: + self.error += [err.MAP_BAD_RENAME_DIRECTIVE % name] + if not (word[0][0] + word[0][-1]).isalnum() or word[0] != self.norm(word[0]).replace(' ', ''): + self.error += [err.MAP_INVALID_LOC_ABBREV % word[0]] + + # Rename no longer supported + # Making sure place not already there + if old_name: + self.error += [err.MAP_RENAME_NOT_SUPPORTED] + if name in self.keywords: + self.error += [err.MAP_LOC_RESERVED_KEYWORD % name] + normed = name + else: + normed = self.norm(name) + if name in self.loc_name or normed in self.aliases: + self.error += [err.MAP_DUP_LOC_OR_POWER % name] + self.loc_name[name] = self.aliases[normed] = word[0] + + # Ambiguous place names end with a ? + for alias in word[1:]: + unclear = alias[-1] == '?' + # For ambiguous place names, let's do just a minimal normalization + # otherwise they might become unrecognizable (e.g. "THE") + normed = alias[:-1].replace('+', ' ').upper() if unclear else self.norm(alias) + if unclear: + self.unclear[normed] = word[0] + elif normed in self.aliases: + if self.aliases[normed] != word[0]: + self.error += [err.MAP_DUP_ALIAS_OR_POWER % alias] + else: + self.aliases[normed] = word[0] + + # ------------------------------------ + # Center ownership (!= Home Ownership) + # -- OWNS center... + # -- CENTERS [center...] + elif upword in ('OWNS', 'CENTERS'): + if not power: + self.error += [err.MAP_OWNS_BEFORE_POWER % (upword, ' '.join(word))] + else: + if power not in self.owns: + self.owns.append(power) + # CENTERS resets the list of centers, OWNS only appends + if upword[0] == 'C' or power not in self.centers: + self.centers[power] = line.upper().split()[1:] + else: + self.centers[power].extend( + [center for center in line.upper().split()[1:] if center not in self.centers[power]]) + + # ------------------------------------ + # Home centers, overriding those from the power declaration line + # -- INHABITS center... + elif upword == 'INHABITS': + if not power: + self.error += [err.MAP_INHABITS_BEFORE_POWER % ' '.join(word)] + else: + reinit = power not in self.inhabits + if reinit: + self.inhabits.append(power) + self.add_homes(power, word[1:], reinit) + + # -- HOME(S) [center...] + elif upword in ('HOME', 'HOMES'): + if not power: + self.error += [err.MAP_HOME_BEFORE_POWER % (upword, ' '.join(word))] + else: + if power not in self.inhabits: + self.inhabits.append(power) + self.add_homes(power, word[1:], 1) + + # ------------------------------------ + # Clear known units for a power + # -- UNITS + elif upword == 'UNITS': + if power: + self.units[power] = [] + else: + self.error += [err.MAP_UNITS_BEFORE_POWER] + + # ------------------------------------ + # Unit Designation (A or F) + # -- unit location + elif upword in ('A', 'F'): + unit = ' '.join(word).upper() + if not power: + self.error += [err.MAP_UNIT_BEFORE_POWER] + elif len(word) == 2: + for units in self.units.values(): + for current_unit in units: + if current_unit[2:5] == unit[2:5]: + units.remove(current_unit) + self.units.setdefault(power, []).append(unit) + else: + self.error += [err.MAP_INVALID_UNIT % unit] + + # ------------------------------------ + # Dummies + # -- DUMMY [ALL] -or- + # -- DUMMY [ALL EXCEPT] powerName... -or- + # -- DUMMIES ALL -or- + # -- DUMMIES [ALL EXCEPT] powerName... + elif upword in ('DUMMY', 'DUMMIES'): + if len(word) > 1: + power = None + # DUMMY + if len(word) == 1: + if upword == 'DUMMIES': + self.error += [err.MAP_DUMMY_REQ_LIST_POWERS] + elif not power: + self.error += [err.MAP_DUMMY_BEFORE_POWER] + elif power not in self.dummies: + self.dummies += [power] + # DUMMY powerName powerName + elif word[1].upper() != 'ALL': + self.dummies.extend( + [dummy for dummy in [self.norm_power(p_name) for p_name in word[1:]] + if dummy not in self.dummies]) + # DUMMY ALL + elif len(word) == 2: + self.dummies = [power_name for power_name in self.homes if power_name != 'UNOWNED'] + # DUMMY ALL powerName + elif word[2].upper() != 'EXCEPT': + self.error += [err.MAP_NO_EXCEPT_AFTER_DUMMY_ALL % upword] + # DUMMY ALL EXCEPT + elif len(word) == 3: + self.error += [err.MAP_NO_POWER_AFTER_DUMMY_ALL_EXCEPT % upword] + # DUMMY ALL EXCEPT powerName powerName + else: + self.dummies = [power_name for power_name in self.homes if power_name not in + (['UNOWNED'] + [self.norm_power(except_pow) for except_pow in word[3:]])] + + # ------------------------------------ + # -- DROP abbreviation... + elif upword == 'DROP': + for place in [loc.upper() for loc in word[1:]]: + self.drop(place) + + # ------------------------------------ + # Terrain type and adjacencies (with special adjacency rules) + # -- COAST abbreviation [ABUTS [abut...]] -or- + # -- LAND abbreviation [ABUTS [abut...]] -or- + # -- WATER abbreviation [ABUTS [abut...]] -or- + # -- PORT abbreviation [ABUTS [abut...]] -or- + # -- SHUT abbreviation [ABUTS [abut...]] -or- + # -- AMEND abbreviation [ABUTS [abut...]] + # -- - removes an abut + elif len(word) > 1 and upword in ('AMEND', 'WATER', 'LAND', 'COAST', 'PORT', 'SHUT'): + place, other = word[1], word[1].swapcase() + + # Removing the place and all its coasts + if other in self.locs: + self.locs.remove(other) + if upword == 'AMEND': + self.loc_type[place] = self.loc_type[other] + self.loc_abut[place] = self.loc_abut[other] + del self.loc_type[other] + del self.loc_abut[other] + if place.isupper(): + for loc in self.locs: + if loc.startswith(place): + self.drop(loc) + if place in self.locs: + self.locs.remove(place) + + # Re-adding the place and its type + self.locs += [place] + if upword != 'AMEND': + self.loc_type[place] = word[0] + if len(word) > 2: + self.loc_abut[place] = [] + elif place not in self.loc_type: + self.error += [err.MAP_NO_DATA_TO_AMEND_FOR % place] + if len(word) > 2 and word[2].upper() != 'ABUTS': + self.error += [err.MAP_NO_ABUTS_FOR % place] + + # Processing ABUTS (adjacencies) + for dest in word[3:]: + + # Removing abuts if they start with - + if dest[0] == '-': + for site in self.loc_abut[place][:]: + if site.upper().startswith(dest[1:].upper()): + self.loc_abut[place].remove(site) + continue + + # Now add the adjacency + self.loc_abut[place] += [dest] + + # ------------------------------------ + # Removal of an existing power + # -- UNPLAYED [ALL] -or- + # -- UNPLAYED [ALL EXCEPT] powerName... + elif upword == 'UNPLAYED': + goners = [] + # UNPLAYED powerName + if len(word) == 1: + if not power: + self.error += [err.MAP_UNPLAYED_BEFORE_POWER] + else: + goners = [power] + # UNPLAYED powerName powerName + elif word[1].upper() != 'ALL': + goners = [self.norm_power(power_name) for power_name in word[1:]] + # UNPLAYED ALL + elif len(word) == 2: + goners = [power_name for power_name in self.homes if power_name != 'UNOWNED'] + # UNPLAYED ALL playerName + elif word[2].upper() != 'EXCEPT': + self.error += [err.MAP_NO_EXCEPT_AFTER_UNPLAYED_ALL] + # UNPLAYED ALL EXCEPT + elif len(word) == 3: + self.error += [err.MAP_NO_POWER_AFTER_UNPLAYED_ALL_EXCEPT] + # UNPLAYED ALL EXCEPT powerName + else: + goners = [power_name for power_name in self.homes if power_name not in + (['UNOWNED'] + [self.norm_power(pow_except) for pow_except in word[3:]])] + + # Removing each power + for goner in goners: + try: + del self.pow_name[goner] + del self.own_word[goner] + del self.homes[goner] + self.dummies = [x for x in self.dummies if x != goner] + self.inhabits = [x for x in self.inhabits if x != goner] + if goner in self.centers: + del self.centers[goner] + self.owns = [x for x in self.owns if x != goner] + if goner in self.abbrev: + del self.abbrev[goner] + if goner in self.units: + del self.units[goner] + self.powers = [x for x in self.powers if x != goner] + except KeyError: + self.error += [err.MAP_NO_SUCH_POWER_TO_REMOVE % goner] + power = None + + else: + # ------------------------------------ + # Power name, ownership word, and home centers + # -- [oldName ->] powerName [([ownWord][:[abbrev]])] [center...] + # -- UNOWNED [center...] -or- + # -- NEUTRAL [center...] -or- + # -- CENTERS [center...] + if upword in ('NEUTRAL', 'CENTERS'): + upword = 'UNOWNED' + power = self.norm_power(upword) if upword != 'UNOWNED' else 0 + + # Renaming power (Not Supported) + if len(word) > 2 and word[1] == '->': + old_power = power + word = word[2:] + upword = word[0].upper() + if upword in ('NEUTRAL', 'CENTERS'): + upword = 'UNOWNED' + power = self.norm_power(upword) if upword != 'UNOWNED' else 0 + if not old_power or not power: + self.error += [err.MAP_RENAMING_UNOWNED_DIR_NOT_ALLOWED] + elif not self.pow_name.get(old_power): + self.error += [err.MAP_RENAMING_UNDEF_POWER % old_power] + else: + self.error += [err.MAP_RENAMING_POWER_NOT_SUPPORTED] + + # Adding power + if power and not self.pow_name.get(power): + self.pow_name[power] = upword + normed = self.norm(power) + # Add power to aliases even if the normed form is identical. That way + # it becomes part of the vocabulary. + if not normed: + self.error += [err.MAP_POWER_NAME_EMPTY_KEYWORD % power] + normed = power + if normed not in self.aliases: + if len(normed.split('/')[0]) in (1, 3): + self.error += [err.MAP_POWER_NAME_CAN_BE_CONFUSED % normed] + self.aliases[normed] = power + elif self.aliases[normed] != power: + self.error += [err.MAP_DUP_LOC_OR_POWER % normed] + + # Processing own word and abbreviations + upword = power or upword + if power and len(word) > 1 and word[1][0] == '(': + self.own_word[upword] = word[1][1:-1] or power + normed = self.norm(self.own_word[upword]) + if normed == power: + pass + elif normed not in self.aliases: + self.aliases[normed] = power + elif self.aliases[normed] != power: + self.error += [err.MAP_DUP_LOC_OR_POWER % normed] + if ':' in word[1]: + owner, abbrev = self.own_word[upword].split(':') + self.own_word[upword] = owner or power + self.abbrev[upword] = abbrev[:1].upper() + if not abbrev or self.abbrev[upword] in 'M?': + self.error += [err.MAP_ILLEGAL_POWER_ABBREV] + del word[1] + else: + self.own_word.setdefault(upword, upword) + + # Adding homes + reinit = upword in self.inhabits + if reinit: + self.inhabits.remove(upword) + self.add_homes(upword, word[1:], reinit) + + def build_cache(self): + """ Builds a cache to speed up abuts and coasts lookup """ + # Adding all coasts to loc_coasts + for loc in self.locs: + self.loc_coasts[loc.upper()] = \ + [map_loc.upper() for map_loc in self.locs if loc.upper()[:3] == map_loc.upper()[:3]] + + # Building abuts cache + for unit_type in ['A', 'F']: + for unit_loc in self.locs: + for other_loc in self.locs: + for order_type in ['-', 'S', 'C']: + + # Calculating and setting in cache + unit_loc, other_loc = unit_loc.upper(), other_loc.upper() + query_tuple = (unit_type, unit_loc, order_type, other_loc) + self.abuts_cache[query_tuple] = self._abuts(*query_tuple) + + 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 reinit: Indicates that we want to strip the list of homes before adding + :return: Nothing + """ + # Reset homes + if reinit: + self.homes[power] = [] + else: + self.homes.setdefault(power, []) + self.homes.setdefault('UNOWNED', []) + + # For each home: + # '-' indicates we want to remove home + for home in ' '.join(homes).upper().split(): + remove = 0 + while home: + if home[0] == '-': + remove = 1 + else: + break + home = home[1:] + if not home: + continue + + # Removing the home if already there + if home in self.homes[power]: + self.homes[power].remove(home) + if power != 'UNOWNED': + self.homes['UNOWNED'].append(home) + + # Re-adding it + if not remove: + self.homes[power].append(home) + + def drop(self, place): + """ Drop a place + :param place: Name of place to remove + :return: Nothing + """ + # Removing from locs + for loc in list(self.locs): + if loc.upper().startswith(place): + self.locs.remove(loc) + + # Loc_name + for full_name, loc in list(self.loc_name.items()): + if loc.startswith(place): + self.loc_name.pop(full_name) + + # Aliases + for alias, loc in list(self.aliases.items()): + if loc.startswith(place): + self.aliases.pop(alias) + + # Homes + for power_name, power_homes in list(self.homes.items()): + if place in power_homes: + self.homes[power_name].remove(place) + + # Units + for power_name, power_units in list(self.units.items()): + for unit in power_units: + if unit[2:5] == place[:3]: + self.units[power_name].remove(unit) + + # Supply Centers + for center in list(self.scs): + if center.upper().startswith(place): + self.scs.remove(center) + + # Centers ownerships + for power_name, power_centers in list(self.centers.items()): + for center in power_centers: + if center.startswith(place): + self.centers[power_name].remove(center) + + # Removing from adjacencies list + for site_name, site_abuts in list(self.loc_abut.items()): + for site in [loc for loc in site_abuts if loc.upper().startswith(place)]: + self.loc_abut[site_name].remove(site) + if site_name.startswith(place): + self.loc_abut.pop(site_name) + + # Removing loc_type + for loc in list(self.loc_type): + if loc.startswith(place): + self.loc_type.pop(loc) + + def norm_power(self, power): + """ Normalise the name of a power (removes spaces) + :param power: Name of power to normalise + :return: Normalised power name + """ + return self.norm(power).replace(' ', '') + + def norm(self, phrase): + """ Normalise a sentence (add spaces before /, replace -+, with ' ', remove .: + :param phrase: Phrase to normalise + :return: Normalised sentences + """ + phrase = phrase.upper().replace('/', ' /').replace(' / ', '') + for token in '.:': + phrase = phrase.replace(token, '') + for token in '-+,': + phrase = phrase.replace(token, ' ') + for token in '|*?!~()[]=_^': + phrase = phrase.replace(token, ' {} '.format(token)) + + # Replace keywords which, contrary to aliases, all consist of a single word + return ' '.join([self.keywords.get(keyword, keyword) for keyword in phrase.strip().split()]) + + 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*)') + :return: The compacted phrase in an array (e.g. ['ENGLAND', 'F', 'WES', 'TYS', '|']) + """ + word, result = self.norm(phrase).split(), [] + while word: + alias, i = self.alias(word) + if alias: + result += alias.split() + word = word[i:] + return result + + 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 + """ + # pylint: disable=too-many-return-statements + # Assume that word already was subject to norm() + # Process with content inside square or round brackets + j = -1 + alias = word[0] + if alias in '([': + for j in range(1, len(word)): + if word[j] == '])'[alias == '(']: + break + else: + return alias, 1 + if j == 1: + return '', 2 + if word[1] + word[j - 1] == '**': + word2 = word[2:j - 1] + else: + word2 = word[1:j] + alias2 = self.aliases.get(' '.join(word2) + ' \\', '') + if alias2[-2:] == ' \\': + return alias2[:-2], j + 1 + result = [] + while word2: + alias2, i = self.alias(word2) + if alias2: + result += [alias2] + word2 = word2[i:] + return ' '.join(result), j + 1 + for i in range(len(word), 0, -1): + key = ' '.join(word[:i]) + if key in self.aliases: + alias = self.aliases[key] + break + else: + i = 1 + + # Concatenate coasts + if i == len(word): + return alias, i + if alias[:1] != '/' and ' ' not in alias: + alias2, j = self.alias(word[i:]) + if alias2[:1] != '/' or ' ' in alias2: + return alias, i + elif alias[-2:] == ' \\': + alias2, j = self.alias(word[i:]) + if alias2[:1] == '/' or ' ' in alias2: + return alias, i + alias, alias2 = alias2, alias[:-2] + else: + return alias, i + + # Check if the location is also an ambiguous power name + # and replace with its other name if that's the case + if alias in self.powers and alias in self.unclear: + alias = self.unclear[alias] + + # Check if the coast is mapped to another coast + if alias + ' ' + alias2 in self.aliases: + return self.aliases[alias + ' ' + alias2], i + j + return alias + alias2, i + j + + 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']) + :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)]) + """ + result = [] + for thing in word: + if ' ' in thing: + data_type = UNDETERMINED + elif len(thing) == 1: + if thing in self.unit_names: + data_type = UNIT + elif thing.isalnum(): + data_type = ORDER + elif thing in '-=_': + data_type = MOVE_SEP + else: + data_type = OTHER + elif '/' in thing: + if thing.find('/') == 3: + data_type = COAST + else: + data_type = POWER + elif thing == 'VIA': + data_type = ORDER + elif len(thing) == 3: + data_type = LOCATION + else: + data_type = POWER + if strict and thing not in list(self.aliases.values()) + list(self.keywords.values()): + data_type = -data_type + result += [(thing, data_type)] + return result + + 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']) + """ + # pylint: disable=too-many-branches + # Add | to start and end of list (to simplify edge cases) (they will be returned as ('|', 7)) + # e.g. [('|', 7), ('A', 2), ('POR', 3), ('S', 5), ('SPA/NC', 4), ('|', 7)] + result = self.vet(['|'] + word + ['|']) + + # Remove result tokens (7) at start and end of string (but keep |) + result[0] = ('|', UNDETERMINED) + while result[-2][1] == OTHER: + del result[-2] + if len(result) == 2: + return [] + result[0] = ('|', OTHER) + while result[1][1] == OTHER: + del result[1] + + # Move "with" unit and location to the start. There should be only one + # Ignore the rest + found = 0 + while ('?', OTHER) in result: + i = result.index(('?', OTHER)) + del result[i] + if found: + continue + j = -1 + for j in range(i, len(result)): + if result[j][1] in (POWER, UNIT): + continue + if result[j][1] in (LOCATION, COAST): + j += 1 + break + if j != i: + found = 1 + k = 0 + for k in range(1, i): + if result[k][1] not in (POWER, UNIT): + break + if k < i: + result[k:k] = result[i:j] + result[j:2 * j - i] = [] + + # Move "from" location before any preceding locations + while ('\\', OTHER) in result: + i = result.index(('\\', OTHER)) + del result[i] + if result[i][1] not in (LOCATION, COAST): + continue + for j in range(i - 1, -1, -1): + if result[j][1] not in (LOCATION, COAST) and result[j][0] != '~': + break + if j + 1 != i: + result[j + 1:j + 1] = result[i:i + 1] + del result[i + 1] + + # Move "via" locations between the two preceding locations. + while ('~', OTHER) in result: + i = result.index(('~', OTHER)) + del result[i] + if (result[i][1] not in (LOCATION, COAST) + or result[i - 1][1] not in (LOCATION, COAST) + or result[i - 2][1] not in (LOCATION, COAST)): + continue + for j in range(i + 1, len(result)): + if result[j][1] not in (LOCATION, COAST): + break + result[j:j] = result[i - 1:i] + del result[i - 1] + + # Move order beyond first location + i = 0 + for j in range(1, len(result)): + if result[j][1] in (LOCATION, COAST): + if i: + result[j + 1:j + 1] = result[i:i + 1] + del result[i] + break + elif result[j][1] == ORDER: + i = j + elif result[j][0] == '|': + break + + # Put the power before the unit, or replace it with a location if there's ambiguity + vet = 0 + for i, result_i in enumerate(result): + if result_i[1] == POWER: + if vet > 0 and result_i[0] in self.unclear: + result[i] = (self.unclear[result_i[0]], LOCATION) + elif vet == 1: + result[i + 1:i + 1] = result[i - 1:i] + del result[i - 1] + vet = 2 + elif not vet and result_i[1] == UNIT: + vet = 1 + elif result_i[1] == ORDER: + vet = 0 + else: + vet = 2 + + # Insert hyphens between subsequent locations + for i in range(len(result) - 1, 1, -1): + if result[i][1] in (LOCATION, COAST) and result[i - 1][1] in (LOCATION, COAST): + result[i:i] = [('-', MOVE_SEP)] + + # Remove vertical bars at start and end + return [x for x, y in result[1:-1]] + + 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') + """ + return self.loc_type.get(loc.upper()) or self.loc_type.get(loc.lower()) + + 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']) + """ + if len(word) == 4 and word[0] == 'F' and word[2] == '-' and '/' not in word[3]: + unit_loc, new_loc, single_coast = word[1], word[3], None + for place in self.abut_list(unit_loc): + up_place = place.upper() + if new_loc == up_place: # Target location found with no coast, original query is correct + return word + if new_loc == up_place[:3]: + if single_coast: # Target location has multiple coasts, unable to decide + return word + single_coast = up_place # Found a potential candidate, storing it + word[3] = single_coast or new_loc # Only one candidate found, modifying the order to include it + return word + + 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 + + :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) + :param other_loc: The location of the other unit + :return: 1 if the locations are adjacent for the move, 0 otherwise + """ + if unit_type == '?': + return (self.abuts_cache.get(('A', unit_loc.upper(), order_type, other_loc.upper()), 0) or + self.abuts_cache.get(('F', unit_loc.upper(), order_type, other_loc.upper()), 0)) + + query_tuple = (unit_type, unit_loc.upper(), order_type, other_loc.upper()) + return self.abuts_cache.get(query_tuple, 0) + + 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 + + :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) + :param other_loc: The location of the other unit + :return: 1 if the locations are adjacent for the move, 0 otherwise + """ + # pylint: disable=too-many-return-statements + unit_loc, other_loc = unit_loc.upper(), other_loc.upper() + + # Removing coasts for support + # Otherwise, if army, not adjacent since army can't move, hold, or convoy on coasts + if '/' in other_loc: + if order_type == 'S': + other_loc = other_loc[:3] + elif unit_type == 'A': + return 0 + + # Looking for adjacency between unit_loc and other_loc + # If the break line is not executed, not adjacency were found + place = '' + for place in self.abut_list(unit_loc): + up_place = place.upper() + up_loc = up_place[:3] + if other_loc in (up_place, up_loc): + break + else: + return 0 + + # If the target location is impassible, returning 0 + other_loc_type = self.area_type(other_loc) + if other_loc_type == 'SHUT': + return 0 + + # If the unit type is unknown, then assume the adjacency is okay + if unit_type == '?': + return 1 + + # Fleets cannot affect LAND and fleets are not adjacent to any location listed in lowercase + # (except when offering support into such an area, as in F BOT S A MOS-STP), or listed in + # the adjacency list in lower-case (F VEN-TUS) + + # Fleet should be supporting a adjacent 'COAST', 'WATER' or 'PORT', with a name starting with a capital letter + if unit_type == 'F': + if (other_loc_type == 'LAND' + or place[0] != up_loc[0] + or order_type != 'S' + and other_loc not in self.loc_type): + return 0 + + # Armies cannot move to water (unless this is a convoy). Note that the caller + # is responsible for determining if a fleet exists at the adjacent spot to convoy + # the army. Also, armies can't move to spaces listed in Mixed case. + elif order_type != 'C' and (other_loc_type == 'WATER' or place == place.title()): + return 0 + + # It's adjacent. + return 1 + + 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 + :return: A boolean to indicate if the unit/location combination is valid + """ + unit_type, loc = unit.upper().split() + area_type = self.area_type(loc) + if area_type == 'SHUT': + return 1 if shut_ok else 0 + if unit_type == '?': + return 1 if area_type is not None else 0 + # Army can be anywhere, except in 'WATER' + if unit_type == 'A': + return '/' not in loc and area_type in ('LAND', 'COAST', 'PORT') + # Fleet must be in WATER, COAST, or PORT + # Coastal locations are stored in CAPS with coasts and non-caps with non-coasts + # e.g. SPA/NC, SPA/SC, spa + return (unit_type == 'F' + and area_type in ('WATER', 'COAST', 'PORT') + and (no_coast_ok or loc.lower() not in self.loc_abut)) + + 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 + :return: A list of adjacent provinces + + Note: abuts are returned in mixed cases (lowercase for A only, First capital letter for F only) + """ + if site in self.loc_abut: + abut_list = self.loc_abut.get(site, []) + else: + abut_list = self.loc_abut.get(site.lower(), []) + if incl_no_coast: + abut_list = abut_list[:] + for loc in list(abut_list): + if '/' in loc and loc[:3] not in abut_list: + abut_list += [loc[:3]] + return abut_list + + 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 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) + """ + # If len < 3, Phase is FORMING or COMPLETED, unable to find previous phase + now = phase.split() + if len(now) < 3: + return phase + + # Parsing year and season index + year = int(now[1]) + season_ix = (self.seq.index('%s %s' % (now[0], now[2])) + 1) % len(self.seq) + seq_len = len(self.seq) + + # Parsing the sequence of seasons + while seq_len: + seq_len -= 1 + new = self.seq[season_ix].split() + + # Looking for IFYEARDIV DIV or IFYEARDIV DIV=MOD + if new[0] == 'IFYEARDIV': + if '=' in new[1]: + div, mod = map(int, new[1].split('=')) + else: + div, mod = int(new[1]), 0 + if year % div != mod: + season_ix = -1 + + # NEWYEAR [X] indicates to increase years by [X] (or 1 by default) + elif new[0] == 'NEWYEAR': + year += len(new) == 1 or int(new[1]) + + # Found phase + elif phase_type in (None, new[1][0]): + if skip == 0: + return '%s %s %s' % (new[0], year, new[1]) + skip -= 1 + seq_len = len(self.seq) + season_ix += 1 + season_ix %= len(self.seq) + + # Could not find next phase + return '' + + 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 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) + """ + # If len < 3, Phase is FORMING or COMPLETED, unable to find previous phase + now = phase.split() + if len(now) < 3: + return phase + + # Parsing year and season index + year = int(now[1]) + season_ix = self.seq.index('%s %s' % (now[0], now[2])) + seq_len = len(self.seq) + + # Parsing the sequence of seasons + while seq_len: + seq_len -= 1 + season_ix -= 1 + + # No more seasons in seq + if season_ix == -1: + for new in [x.split() for x in self.seq]: + # Looking for IFYEARDIV DIV or IFYEARDIV DIV=MOD + if new[0] == 'IFYEARDIV': + if '=' in new[1]: + div, mod = map(int, new[1].split('=')) + else: + div, mod = int(new[1]), 0 + if year % div != mod: + break + season_ix += 1 + + # Parsing next seq + new = self.seq[season_ix].split() + if new[0] == 'IFYEARDIV': + pass + + # NEWYEAR [X] indicates to increase years by [X] (or 1 by default) + elif new[0] == 'NEWYEAR': + year -= len(new) == 1 or int(new[1]) + + # Found phase + elif phase_type in (None, new[1][0]): + if skip == 0: + return '%s %s %s' % (new[0], year, new[1]) + skip -= 1 + seq_len = len(self.seq) + + # Could not find prev phase + return '' + + 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 + """ + # If the phase ends with '?', we assume it's the last phase type of that season + # e.g. S1901? -> S1901R W1901? -> W1901A + if phase1[-1] == '?': + phase1 = phase1[:-1] + [season.split()[1][0] for season in self.seq if season[0] == phase1[0]][-1] + if phase2[-1] == '?': + phase2 = phase2[:-1] + [season.split()[1][0] for season in self.seq if season[0] == phase2[0]][-1] + + # Converting S1901M (abbrv) to long phase (SPRING 1901 MOVEMENT) + if len(phase1.split()) == 1: + phase1 = self.phase_long(phase1, phase1.upper()) + if len(phase2.split()) == 1: + phase2 = self.phase_long(phase2, phase2.upper()) + if phase1 == phase2: + return 0 + now1, now2 = phase1.split(), phase2.split() + + # One of the phase is either FORMING, or COMPLETED + # Syntax is (bool1 and int1 or bool2 and int2) will return int1 if bool1, else int2 if bool2 + # 1 = FORMING, 2 = Normal Phase, 3 = COMPLETED, 0 = UNKNOWN + if len(now1) < 3 or len(now2) < 3: + order1 = (len(now1) > 2 and 2 or phase1 == 'FORMING' and 1 or phase1 == 'COMPLETED' and 3 or 0) + order2 = (len(now2) > 2 and 2 or phase2 == 'FORMING' and 1 or phase2 == 'COMPLETED' and 3 or 0) + return order1 > order2 and 1 or order1 < order2 and -1 or 0 + + # Comparing years + year1, year2 = int(now1[1]), int(now2[1]) + if year1 != year2: + return (year1 > year2 and 1 or -1) * (self.flow_sign or 1) + + # Comparing seasons + # Returning the inverse if NEW_YEAR is between the 2 seasons + season_ix1 = self.seq.index('%s %s' % (now1[0], now1[2])) + season_ix2 = self.seq.index('%s %s' % (now2[0], now2[2])) + if season_ix1 > season_ix2: + return -1 if 'NEWYEAR' in [x.split()[0] for x in self.seq[(season_ix2) + (1):season_ix1]] else 1 + if season_ix1 < season_ix2: + return 1 if 'NEWYEAR' in [x.split()[0] for x in self.seq[(season_ix1) + (1):season_ix2]] else -1 + return 0 + + @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 + """ + if phase in ('FORMING', 'COMPLETED'): + return phase + parts = tuple(phase.split()) + return ('%.1s%04d%.1s' % (parts[0], int(parts[1]), parts[2])).upper() if len(parts) == 3 else default + + 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) + """ + try: + year = int(phase_abbr[1:-1]) + for season in self.seq: + parts = season.split() + if parts[0] not in ('NEWYEAR', 'IFYEARDIV') \ + and parts[0][0].upper() == phase_abbr[0].upper() \ + and parts[1][0].upper() == phase_abbr[-1].upper(): + return '{} {} {}'.format(parts[0], year, parts[1]).upper() + except ValueError: + pass + return default + +# Loading at the bottom, to avoid load recursion +from diplomacy.utils.convoy_paths import add_to_cache, get_convoy_paths_cache # pylint: disable=wrong-import-position +CONVOYS_PATH_CACHE = get_convoy_paths_cache() diff --git a/diplomacy/engine/message.py b/diplomacy/engine/message.py new file mode 100644 index 0000000..2d6d644 --- /dev/null +++ b/diplomacy/engine/message.py @@ -0,0 +1,115 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Game message. Represent a message exchanged inside a game. + + Possible messages exchanges: + - power 1 -> power 2 + - power -> all game + - system -> power + - system -> all game + - system -> observers + - 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. + + 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. + Use constant names defined below to specify recipient for system message when it's not a power name + (GLOBAL, OBSERVER or OMNISCIENT). +""" +from diplomacy.utils import parsing, strings +from diplomacy.utils.jsonable import Jsonable + +SYSTEM = 'SYSTEM' # sender +GLOBAL = 'GLOBAL' # recipient (all powers) +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: + 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 + are stored on server. Therefore, message timestamp is the time when server stores the message, not the time + when message was sent by any client. + """ + __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.TIME_SENT: parsing.OptionalValueType(int), # given by server. + strings.PHASE: str, # phase short name. + 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 + super(Message, self).__init__(**kwargs) + + def __str__(self): + return '[%d/%s/%s->%s](%s)' % ( + self.time_sent, self.phase, self.sender, self.recipient, self.message) + + def __hash__(self): + return hash(self.time_sent) + + def __eq__(self, other): + assert isinstance(other, Message) + return self.time_sent == other.time_sent + + def __ne__(self, other): + assert isinstance(other, Message) + return self.time_sent != other.time_sent + + def __lt__(self, other): + assert isinstance(other, Message) + return self.time_sent < other.time_sent + + def __gt__(self, other): + assert isinstance(other, Message) + return self.time_sent > other.time_sent + + def __le__(self, other): + assert isinstance(other, Message) + return self.time_sent <= other.time_sent + + def __ge__(self, other): + assert isinstance(other, Message) + return self.time_sent >= other.time_sent + + def is_global(self): + """ Return True if this message is global. """ + return self.recipient == GLOBAL + + def for_observer(self): + """ Return True if this message is sent to observers. """ + return self.recipient == OBSERVER diff --git a/diplomacy/engine/power.py b/diplomacy/engine/power.py new file mode 100644 index 0000000..570359b --- /dev/null +++ b/diplomacy/engine/power.py @@ -0,0 +1,392 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Power + - Contains the power object representing a power in the game +""" +from copy import deepcopy +from diplomacy.utils import parsing, strings +from diplomacy.utils.exceptions import DiplomacyException +from diplomacy.utils.jsonable import Jsonable +from diplomacy.utils.sorted_dict import SortedDict +from diplomacy.utils import common, constants +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'). + """ + __slots__ = ['game', 'name', 'abbrev', 'adjust', 'centers', 'units', 'influence', 'homes', + 'retreats', 'goner', 'civil_disorder', 'orders', 'role', 'controller', 'vote', + 'order_is_set', 'wait', 'tokens'] + model = { + strings.ABBREV: parsing.OptionalValueType(str), + strings.ADJUST: parsing.DefaultValueType(parsing.SequenceType(str), []), + strings.CENTERS: parsing.DefaultValueType(parsing.SequenceType(str), []), + strings.CIVIL_DISORDER: parsing.DefaultValueType(int, 0), + strings.CONTROLLER: parsing.DefaultValueType(parsing.DictType(int, str, SortedDict.builder(int, str)), {}), + strings.HOMES: parsing.OptionalValueType(parsing.SequenceType(str)), + strings.INFLUENCE: parsing.DefaultValueType(parsing.SequenceType(str), []), + strings.NAME: parsing.PrimitiveType(str), + strings.ORDER_IS_SET: parsing.DefaultValueType(OrderSettings.ALL_SETTINGS, OrderSettings.ORDER_NOT_SET), + strings.ORDERS: parsing.DefaultValueType(parsing.DictType(str, str), {}), + strings.RETREATS: parsing.DefaultValueType(parsing.DictType(str, parsing.SequenceType(str)), {}), + strings.ROLE: parsing.DefaultValueType(str, strings.SERVER_TYPE), + strings.TOKENS: parsing.DefaultValueType(parsing.SequenceType(str, set), ()), + strings.UNITS: parsing.DefaultValueType(parsing.SequenceType(str), []), + strings.VOTE: parsing.DefaultValueType(parsing.EnumerationType(strings.ALL_VOTE_DECISIONS), strings.NEUTRAL), + strings.WAIT: parsing.DefaultValueType(bool, True), + } + + def __init__(self, game=None, name=None, **kwargs): + """ Constructor """ + self.game = game + self.abbrev = None + self.adjust, self.centers, self.units, self.influence = [], [], [], [] + self.homes = None + self.retreats = {} + self.goner = self.civil_disorder = 0 + self.orders = {} + + self.name = '' + self.role = '' + self.controller = SortedDict(int, str) + self.vote = '' + self.order_is_set = 0 + self.wait = False + self.tokens = set() + super(Power, self).__init__(name=name, **kwargs) + assert self.role in strings.ALL_ROLE_TYPES or self.role == self.name + if not self.controller: + self.controller.put(common.timestamp_microseconds(), strings.DUMMY) + + def __str__(self): + """ Returns a representation of the power instance """ + show_cd = self.civil_disorder + show_inhabits = self.homes is not None + show_owns = self.centers + show_retreats = len(self.retreats) > 0 + + text = '' + text += '\n%s (%s)' % (self.name, self.role) + text += '\nPLAYER %s' % self.controller.last_value() + text += '\nCD' if show_cd else '' + text += '\nINHABITS %s' % ' '.join(self.homes) if show_inhabits else '' + text += '\nOWNS %s' % ' '.join(self.centers) if show_owns else '' + if show_retreats: + text += '\n'.join([''] + [' '.join([unit, '-->'] + places) for unit, places in self.retreats.items()]) + text = '\n'.join([text] + self.units + self.adjust) + + # Orders - RIO is for REORDER, INVALID, ORDER (in NO_CHECK games) + text_order = '\nORDERS\n' + for unit, order in self.orders.items(): + if unit[0] not in 'RIO': + text_order += '%s ' % unit + text_order += order + '\n' + + text += text_order if self.orders else '' + return text + + def __deepcopy__(self, memo): + """ Fast deep copy implementation + - (Not setting the game object) + """ + cls = self.__class__ + result = cls.__new__(cls) + + # Deep copying + for key in self.__slots__: + if key not in ['game']: + setattr(result, key, deepcopy(getattr(self, key))) + + # Game + setattr(result, 'game', None) + return result + + 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 + """ + reinit_persistent = include_flags & 2 + reinit_transient = include_flags & 4 + reinit_orders = include_flags & 1 + + # Initialize the persistent parameters + if reinit_persistent: + self.abbrev = None + + # Initialize the transient parameters + if reinit_transient: + for home in self.homes: + self.game.update_hash(self.name, loc=home, is_home=True) + for center in self.centers: + self.game.update_hash(self.name, loc=center, is_center=True) + for unit in self.units: + self.game.update_hash(self.name, unit_type=unit[0], loc=unit[2:]) + for dis_unit in self.retreats: + self.game.update_hash(self.name, unit_type=dis_unit[0], loc=dis_unit[2:], is_dislodged=True) + self.homes = None + self.centers, self.units, self.influence = [], [], [] + self.retreats = {} + + # Initialize the order-related parameters + if reinit_orders: + self.civil_disorder = 0 + self.adjust = [] + self.orders = {} + if self.is_eliminated(): + self.order_is_set = OrderSettings.ORDER_SET_EMPTY + self.wait = False + else: + self.order_is_set = OrderSettings.ORDER_NOT_SET + self.wait = True if self.is_dummy() else (not self.game.real_time) + self.goner = 0 + + @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 + """ + cmp = lambda power_1, power_2: ((power_1 > power_2) - (power_1 < power_2)) + xstr = lambda string: string or '' # To avoid comparing with None + cmp_type = cmp(xstr(power_1.role), xstr(power_2.role)) + cmp_name = cmp(xstr(power_1.name), xstr(power_2.name)) + return cmp_type or cmp_name + + 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 + """ + + # Not initializing observers and monitors + assert self.is_server_power() + + self.game = game + self.order_is_set = OrderSettings.ORDER_NOT_SET + self.wait = True if self.is_dummy() else (not self.game.real_time) + + # Get power abbreviation. + self.abbrev = self.game.map.abbrev.get(self.name, self.name[0]) + + # Resets homes + if self.homes is None: + self.homes = [] + for home in game.map.homes.get(self.name, []): + self.game.update_hash(self.name, loc=home, is_home=True) + self.homes.append(home) + + # Resets the centers and units + if not self.centers: + for center in game.map.centers.get(self.name, []): + game.update_hash(self.name, loc=center, is_center=True) + self.centers.append(center) + if not self.units: + for unit in game.map.units.get(self.name, []): + game.update_hash(self.name, unit_type=unit[0], loc=unit[2:]) + self.units.append(unit) + self.influence.append(unit[2:5]) + + 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 + for unit in list(other_power.units): + self.units.append(unit) + other_power.units.remove(unit) + self.game.update_hash(self.name, unit_type=unit[0], loc=unit[2:]) + self.game.update_hash(other_power.name, unit_type=unit[0], loc=unit[2:]) + + # Dislodged units + for unit in list(other_power.retreats.keys()): + self.retreats[unit] = other_power.retreats[unit] + del other_power.retreats[unit] + self.game.update_hash(self.name, unit_type=unit[0], loc=unit[2:], is_dislodged=True) + self.game.update_hash(other_power.name, unit_type=unit[0], loc=unit[2:], is_dislodged=True) + + # Influence + for loc in list(other_power.influence): + self.influence.append(loc) + other_power.influence.remove(loc) + + # Supply centers + for center in list(other_power.centers): + self.centers.append(center) + other_power.centers.remove(center) + self.game.update_hash(self.name, loc=center, is_center=True) + self.game.update_hash(other_power.name, loc=center, is_center=True) + + # Homes + for home in list(other_power.homes): + self.homes.append(home) + other_power.homes.remove(home) + self.game.update_hash(self.name, loc=home, is_home=True) + self.game.update_hash(other_power.name, loc=home, is_home=True) + + # Clearing state cache + self.game.clear_cache() + + def clear_units(self): + """ Removes all units from the map """ + for unit in self.units: + self.game.update_hash(self.name, unit_type=unit[0], loc=unit[2:]) + self.units = [] + self.influence = [] + self.game.clear_cache() + + def clear_centers(self): + """ Removes ownership of all supply centers """ + for center in self.centers: + self.game.update_hash(self.name, loc=center, is_center=True) + self.centers = [] + self.game.clear_cache() + + 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 + if self.units or self.centers or self.retreats: + return False + return True + + def clear_orders(self): + """ Clears the power's orders """ + self.reinit(include_flags=1) + + 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': + return 1 + return self.orders or not self.units + + # ============================================================== + # Application/network methods (mainly used for connected games). + # ============================================================== + + def is_observer_power(self): + """ 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. """ + return self.role == strings.OMNISCIENT_TYPE + + def is_player_power(self): + """ 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. """ + return self.role == strings.SERVER_TYPE + + def is_controlled(self): + """ 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). """ + return self.order_is_set and not self.wait + + def update_controller(self, username, timestamp): + """ 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). """ + if username is None or username == strings.DUMMY: + if self.controller.last_value() != strings.DUMMY: + self.controller.put(common.timestamp_microseconds(), strings.DUMMY) + self.tokens.clear() + self.wait = True + self.vote = strings.NEUTRAL + elif self.controller.last_value() == strings.DUMMY: + self.controller.put(common.timestamp_microseconds(), username) + self.wait = not self.game.real_time + elif self.controller.last_value() != username: + 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). """ + return self.controller.last_value() + + def get_controller_timestamp(self): + """ 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. """ + 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) + return self.controller.last_value() == username + + # Server-only methods. + + def has_token(self, token): + """ 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. """ + assert self.is_server_power() + self.tokens.add(token) + + def remove_tokens(self, tokens): + """ 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 new file mode 100644 index 0000000..8b5e10c --- /dev/null +++ b/diplomacy/engine/renderer.py @@ -0,0 +1,789 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +# -*- coding: utf-8 -*- +""" Renderer + - Contains the renderer object which is responsible for rendering a game state to svg +""" +import os +from xml.dom import minidom +from diplomacy import settings + +# Constants +LAYER_SC = 'SupplyCenterLayer' +LAYER_ORDER = 'OrderLayer' +LAYER_UNIT = 'UnitLayer' +LAYER_DISL = 'DislodgedUnitLayer' +ARMY = 'Army' +FLEET = 'Fleet' + +def _attr(node_element, attr_name): + """ Shorthand method to retrieve an XML attribute """ + return node_element.attributes[attr_name].value + +def _offset(str_float, offset): + """ Shorthand to add a offset to an attribute """ + return str(float(str_float) + offset) + + +class Renderer(): + """ Renderer object responsible for rendering a game state to svg """ + + def __init__(self, game): + """ Constructor + :param game: The instantiated game object to render + :type game: diplomacy.Game + """ + self.game = game + self.metadata = {} + self.xml_map = None + self.xml_map_path = os.path.join(settings.PACKAGE_DIR, 'maps', 'svg', self.game.map.name + '.svg') + + # Loading XML + if os.path.exists(self.xml_map_path): + self.xml_map = minidom.parse(self.xml_map_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. + :return: The rendered image in the specified format. + """ + # pylint: disable=too-many-branches + if output_format not in ['svg']: + raise ValueError('Only "svg" format is current supported.') + if not self.game or not self.game.map or not self.xml_map: + return None + + # Parsing XML + xml_map = minidom.parseString(self.xml_map) + scs = self.game.map.scs[:] + + # Setting phase and note + nb_centers = [(power.name[:3], len(power.centers)) + for power in self.game.powers.values() + if not power.is_eliminated()] + nb_centers = sorted(nb_centers, key=lambda key: key[1], reverse=True) + nb_centers_per_power = ' '.join(['{}: {}'.format(name, centers) for name, centers in nb_centers]) + xml_map = self._set_current_phase(xml_map, self.game.get_current_phase()) + xml_map = self._set_note(xml_map, nb_centers_per_power, self.game.note) + + # Adding units, supply centers, and influence + for power in self.game.powers.values(): + for unit in power.units: + xml_map = self._add_unit(xml_map, unit, power.name, is_dislodged=False) + for unit in power.retreats: + xml_map = self._add_unit(xml_map, unit, power.name, is_dislodged=True) + for center in power.centers: + xml_map = self._add_supply_center(xml_map, center, power.name) + xml_map = self._set_influence(xml_map, center, power.name, has_supply_center=True) + scs.remove(center) + for loc in power.influence: + xml_map = self._set_influence(xml_map, loc, power.name, has_supply_center=False) + + # Orders + if incl_orders: + + # Regular orders (Normalized) + # A PAR H + # A PAR - BUR [VIA] + # A PAR S BUR + # A PAR S F BRE - PIC + # F BRE C A PAR - LON + for order_key in power.orders: + + # No_check order (Order, Invalid, Reorder) + # Otherwise regular order (unit is key, order without unit is value) + if order_key[0] in 'RIO': + order = power.orders[order_key] + else: + order = '{} {}'.format(order_key, power.orders[order_key]) + + # Normalizing and splitting in tokens + tokens = self.norm_order(order) + unit_loc = tokens[1] + + # Parsing based on order type + if not tokens or len(tokens) < 3: + continue + elif tokens[2] == 'H': + xml_map = self._issue_hold_order(xml_map, unit_loc, power.name) + elif tokens[2] == '-': + dest_loc = tokens[-1] if tokens[-1] != 'VIA' else tokens[-2] + xml_map = self._issue_move_order(xml_map, unit_loc, dest_loc, power.name) + elif tokens[2] == 'S': + dest_loc = tokens[-1] + if '-' in tokens: + src_loc = tokens[4] if tokens[3] == 'A' or tokens[3] == 'F' else tokens[3] + xml_map = self._issue_support_move_order(xml_map, unit_loc, src_loc, dest_loc, power.name) + else: + xml_map = self._issue_support_hold_order(xml_map, unit_loc, dest_loc, power.name) + elif tokens[2] == 'C': + src_loc = tokens[4] if tokens[3] == 'A' or tokens[3] == 'F' else tokens[3] + dest_loc = tokens[-1] + if src_loc != dest_loc and '-' in tokens: + xml_map = self._issue_convoy_order(xml_map, unit_loc, src_loc, dest_loc, power.name) + else: + raise RuntimeError('Unknown order: {}'.format(' '.join(tokens))) + + # Adjustment orders + # VOID xxx + # A PAR B + # A PAR D + # A PAR R BUR + # WAIVE + for order in power.adjust: + tokens = order.split() + if not tokens or tokens[0] == 'VOID' or tokens[-1] == 'WAIVE': + continue + elif tokens[-1] == 'B': + if len(tokens) < 3: + continue + xml_map = self._issue_build_order(xml_map, tokens[0], tokens[1], power.name) + elif tokens[-1] == 'D': + xml_map = self._issue_disband_order(xml_map, tokens[1]) + elif tokens[-2] == 'R': + src_loc = tokens[1] if tokens[0] == 'A' or tokens[0] == 'F' else tokens[0] + dest_loc = tokens[-1] + xml_map = self._issue_move_order(xml_map, src_loc, dest_loc, power.name) + else: + raise RuntimeError('Unknown order: {}'.format(order)) + + # Adding remaining supply centers + for center in scs: + xml_map = self._add_supply_center(xml_map, center, None) + + # Removing abbrev and mouse layer + svg_node = xml_map.getElementsByTagName('svg')[0] + for child_node in svg_node.childNodes: + if child_node.nodeName != 'g': + continue + if _attr(child_node, 'id') == 'BriefLabelLayer' and not incl_abbrev: + svg_node.removeChild(child_node) + elif _attr(child_node, 'id') == 'MouseLayer': + svg_node.removeChild(child_node) + + # Returning + return xml_map.toxml() + + def _load_metadata(self): + """ Loads meta-data embedded in the XML map and clears unused nodes """ + if not self.xml_map: + return + xml_map = minidom.parseString(self.xml_map) + + # Data + self.metadata = { + 'color': {}, + 'symbol_size': {}, + 'orders': {}, + 'coord': {} + } + + # Order drawings + for order_drawing in xml_map.getElementsByTagName('jdipNS:ORDERDRAWING'): + for child_node in order_drawing.childNodes: + + # Power Colors + if child_node.nodeName == 'jdipNS:POWERCOLORS': + for power_color in child_node.childNodes: + if power_color.nodeName == 'jdipNS:POWERCOLOR': + self.metadata['color'][_attr(power_color, 'power').upper()] = _attr(power_color, 'color') + + # Symbol size + elif child_node.nodeName == 'jdipNS:SYMBOLSIZE': + self.metadata['symbol_size'][_attr(child_node, 'name')] = (_attr(child_node, 'height'), + _attr(child_node, 'width')) + + # Order type + elif child_node.nodeName.startswith('jdipNS'): + order_type = child_node.nodeName.replace('jdipNS:', '') + self.metadata['orders'][order_type] = {} + for attr_name, attr_value in child_node.attributes.items(): + if ':' in attr_name: + continue + self.metadata['orders'][order_type][attr_name] = attr_value + + # Object coordinates + for province_data in xml_map.getElementsByTagName('jdipNS:PROVINCE_DATA'): + for child_node in province_data.childNodes: + + # Province + if child_node.nodeName == 'jdipNS:PROVINCE': + province = _attr(child_node, 'name').upper().replace('-', '/') + self.metadata['coord'][province] = {} + + for coord_node in child_node.childNodes: + if coord_node.nodeName == 'jdipNS:UNIT': + self.metadata['coord'][province]['unit'] = (_attr(coord_node, 'x'), _attr(coord_node, 'y')) + elif coord_node.nodeName == 'jdipNS:DISLODGED_UNIT': + self.metadata['coord'][province]['disl'] = (_attr(coord_node, 'x'), _attr(coord_node, 'y')) + elif coord_node.nodeName == 'jdipNS:SUPPLY_CENTER': + self.metadata['coord'][province]['sc'] = (_attr(coord_node, 'x'), _attr(coord_node, 'y')) + + # Deleting + svg_node = xml_map.getElementsByTagName('svg')[0] + svg_node.removeChild(xml_map.getElementsByTagName('jdipNS:DISPLAY')[0]) + svg_node.removeChild(xml_map.getElementsByTagName('jdipNS:ORDERDRAWING')[0]) + svg_node.removeChild(xml_map.getElementsByTagName('jdipNS:PROVINCE_DATA')[0]) + self.xml_map = xml_map.toxml() + + 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') + :param is_dislodged: Boolean. Indicates if the unit is dislodged + :return: Nothing + """ + unit_type, loc = unit.split() + symbol = FLEET if unit_type == 'F' else ARMY + loc_x = _offset(self.metadata['coord'][loc][('unit', 'disl')[is_dislodged]][0], -11.5) + loc_y = _offset(self.metadata['coord'][loc][('unit', 'disl')[is_dislodged]][1], - 10.) + node = xml_map.createElement('use') + node.setAttribute('x', loc_x) + node.setAttribute('y', loc_y) + node.setAttribute('height', self.metadata['symbol_size'][symbol][0]) + node.setAttribute('width', self.metadata['symbol_size'][symbol][1]) + node.setAttribute('xlink:href', '#{}{}'.format(('', 'Dislodged')[is_dislodged], symbol)) + node.setAttribute('class', 'unit{}'.format(power_name.lower())) + + # Inserting + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' \ + and _attr(child_node, 'id') == ['UnitLayer', 'DislodgedUnitLayer'][is_dislodged]: + child_node.appendChild(node) + break + return xml_map + + def _add_supply_center(self, xml_map, loc, power_name): + """ Adds a supply center to the map + :param xml_map: The xml map being generated + :param loc: The province where to add the SC (e.g. 'PAR') + :param power_name: The name of the power owning the SC or None + :return: Nothing + """ + symbol = 'SupplyCenter' + loc_x = _offset(self.metadata['coord'][loc]['sc'][0], -8.5) + loc_y = _offset(self.metadata['coord'][loc]['sc'][1], -11.) + node = xml_map.createElement('use') + node.setAttribute('x', loc_x) + node.setAttribute('y', loc_y) + node.setAttribute('height', self.metadata['symbol_size'][symbol][0]) + node.setAttribute('width', self.metadata['symbol_size'][symbol][1]) + node.setAttribute('xlink:href', '#{}'.format(symbol)) + if power_name: + node.setAttribute('class', 'sc{}'.format(power_name.lower())) + else: + node.setAttribute('class', 'scnopower') + + # Inserting + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'SupplyCenterLayer': + child_node.appendChild(node) + break + return xml_map + + 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 + :param has_supply_center: Boolean flag to acknowledge we are modifying a loc with a SC + :return: Nothing + """ + loc = loc.upper()[:3] + if loc in self.game.map.scs and not has_supply_center: + return xml_map + if self.game.map.area_type(loc) not in ['LAND', 'COAST']: + return xml_map + + # Inserting + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'MapLayer': + for map_node in child_node.childNodes: + if map_node.nodeName == 'path' and _attr(map_node, 'id') == '_{}'.format(loc.lower()): + if power_name: + map_node.setAttribute('class', power_name.lower()) + else: + map_node.setAttribute('class', 'nopower') + return xml_map + + # Returning + return xml_map + + @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 + """ + current_phase = 'FINAL' if current_phase[0] == '?' else current_phase + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'text' and _attr(child_node, 'id') == 'CurrentPhase': + child_node.childNodes[0].nodeValue = current_phase + return xml_map + return xml_map + + @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 + :return: Nothing + """ + note_1 = note_1 or ' ' + note_2 = note_2 or ' ' + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'text' and _attr(child_node, 'id') == 'CurrentNote': + child_node.childNodes[0].nodeValue = note_1 + if child_node.nodeName == 'text' and _attr(child_node, 'id') == 'CurrentNote2': + child_node.childNodes[0].nodeValue = note_2 + return xml_map + + 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 + :return: Nothing + """ + # Calculating polygon coord + polygon_coord = [] + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], 8.5) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], 9.5) + for offset in [(13.8, -33.3), (33.3, -13.8), (33.3, 13.8), (13.8, 33.3), (-13.8, 33.3), (-33.3, 13.8), + (-33.3, -13.8), (-13.8, -33.3)]: + polygon_coord += [_offset(loc_x, offset[0]) + ',' + _offset(loc_y, offset[1])] + + # Building polygon + g_node = xml_map.createElement('g') + + poly_1 = xml_map.createElement('polygon') + poly_1.setAttribute('stroke-width', '10') + poly_1.setAttribute('class', 'varwidthshadow') + poly_1.setAttribute('points', ' '.join(polygon_coord)) + + poly_2 = xml_map.createElement('polygon') + poly_2.setAttribute('stroke-width', '6') + poly_2.setAttribute('class', 'varwidthorder') + poly_2.setAttribute('points', ' '.join(polygon_coord)) + poly_2.setAttribute('stroke', self.metadata['color'][power_name]) + + g_node.appendChild(poly_1) + g_node.appendChild(poly_2) + + # Inserting + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'OrderLayer': + for layer_node in child_node.childNodes: + if layer_node.nodeName == 'g' and _attr(layer_node, 'id') == 'Layer1': + layer_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + 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') + :param power_name: The power name issuing the move order + :return: Nothing + """ + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], 10) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], 10) + dest_loc_x = _offset(self.metadata['coord'][dest_loc]['unit'][0], 10) + dest_loc_y = _offset(self.metadata['coord'][dest_loc]['unit'][1], 10) + + # Adjusting destination + delta_x = float(dest_loc_x) - float(loc_x) + delta_y = float(dest_loc_y) - float(loc_y) + vector_length = (delta_x ** 2. + delta_y ** 2.) ** 0.5 + dest_loc_x = str(round(float(loc_x) + (vector_length - 35.) / vector_length * delta_x, 2)) + dest_loc_y = str(round(float(loc_y) + (vector_length - 35.) / vector_length * delta_y, 2)) + + # Getting polygon coordinates + polygon_coord = [] + poly_loc_x = _offset(self.metadata['coord'][dest_loc]['unit'][0], 8.5) + poly_loc_y = _offset(self.metadata['coord'][dest_loc]['unit'][1], 9.5) + for offset in [(15.9, -38.3), (38.3, -15.9), (38.3, 15.9), (15.9, 38.3), (-15.9, 38.3), (-38.3, 15.9), + (-38.3, -15.9), (-15.9, -38.3)]: + polygon_coord += [_offset(poly_loc_x, offset[0]) + ',' + _offset(poly_loc_y, offset[1])] + + # Creating nodes + g_node = xml_map.createElement('g') + + shadow_line = xml_map.createElement('line') + shadow_line.setAttribute('x1', loc_x) + shadow_line.setAttribute('y1', loc_y) + shadow_line.setAttribute('x2', dest_loc_x) + shadow_line.setAttribute('y2', dest_loc_y) + shadow_line.setAttribute('class', 'shadowdash') + + support_line = xml_map.createElement('line') + support_line.setAttribute('x1', loc_x) + support_line.setAttribute('y1', loc_y) + support_line.setAttribute('x2', dest_loc_x) + support_line.setAttribute('y2', dest_loc_y) + support_line.setAttribute('class', 'supportorder') + support_line.setAttribute('stroke', self.metadata['color'][power_name]) + + shadow_poly = xml_map.createElement('polygon') + shadow_poly.setAttribute('class', 'shadowdash') + shadow_poly.setAttribute('points', ' '.join(polygon_coord)) + + support_poly = xml_map.createElement('polygon') + support_poly.setAttribute('class', 'supportorder') + support_poly.setAttribute('points', ' '.join(polygon_coord)) + support_poly.setAttribute('stroke', self.metadata['color'][power_name]) + + # Inserting + g_node.appendChild(shadow_line) + g_node.appendChild(support_line) + g_node.appendChild(shadow_poly) + g_node.appendChild(support_poly) + + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'OrderLayer': + for layer_node in child_node.childNodes: + if layer_node.nodeName == 'g' and _attr(layer_node, 'id') == 'Layer2': + layer_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + 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') + :param power_name: The power name issuing the move order + :return: Nothing + """ + if self.game.get_current_phase()[-1] == 'R': + src_loc_x = _offset(self.metadata['coord'][src_loc]['unit'][0], -2.5) + src_loc_y = _offset(self.metadata['coord'][src_loc]['unit'][1], -2.5) + else: + src_loc_x = _offset(self.metadata['coord'][src_loc]['unit'][0], 10) + src_loc_y = _offset(self.metadata['coord'][src_loc]['unit'][1], 10) + dest_loc_x = _offset(self.metadata['coord'][dest_loc]['unit'][0], 10) + dest_loc_y = _offset(self.metadata['coord'][dest_loc]['unit'][1], 10) + + # Adjusting destination + delta_x = float(dest_loc_x) - float(src_loc_x) + delta_y = float(dest_loc_y) - float(src_loc_y) + vector_length = (delta_x ** 2. + delta_y ** 2.) ** 0.5 + dest_loc_x = str(round(float(src_loc_x) + (vector_length - 30.) / vector_length * delta_x, 2)) + dest_loc_y = str(round(float(src_loc_y) + (vector_length - 30.) / vector_length * delta_y, 2)) + + # Creating nodes + g_node = xml_map.createElement('g') + + line_with_shadow = xml_map.createElement('line') + line_with_shadow.setAttribute('x1', src_loc_x) + line_with_shadow.setAttribute('y1', src_loc_y) + line_with_shadow.setAttribute('x2', dest_loc_x) + line_with_shadow.setAttribute('y2', dest_loc_y) + line_with_shadow.setAttribute('class', 'varwidthshadow') + line_with_shadow.setAttribute('stroke-width', '10') + + line_with_arrow = xml_map.createElement('line') + line_with_arrow.setAttribute('x1', src_loc_x) + line_with_arrow.setAttribute('y1', src_loc_y) + line_with_arrow.setAttribute('x2', dest_loc_x) + line_with_arrow.setAttribute('y2', dest_loc_y) + line_with_arrow.setAttribute('class', 'varwidthorder') + line_with_arrow.setAttribute('stroke', self.metadata['color'][power_name]) + line_with_arrow.setAttribute('stroke-width', '6') + line_with_arrow.setAttribute('marker-end', 'url(#arrow)') + + # Inserting + g_node.appendChild(line_with_shadow) + g_node.appendChild(line_with_arrow) + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'OrderLayer': + for layer_node in child_node.childNodes: + if layer_node.nodeName == 'g' and _attr(layer_node, 'id') == 'Layer1': + layer_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + 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') + :param dest_loc: The location where the unit is moving to (e.g. 'MAR') + :param power_name: The power name issuing the move order + :return: Nothing + """ + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], 10) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], 10) + src_loc_x = _offset(self.metadata['coord'][src_loc]['unit'][0], 10) + src_loc_y = _offset(self.metadata['coord'][src_loc]['unit'][1], 10) + dest_loc_x = _offset(self.metadata['coord'][dest_loc]['unit'][0], 10) + dest_loc_y = _offset(self.metadata['coord'][dest_loc]['unit'][1], 10) + + # Adjusting destination + delta_x = float(dest_loc_x) - float(src_loc_x) + delta_y = float(dest_loc_y) - float(src_loc_y) + vector_length = (delta_x ** 2. + delta_y ** 2.) ** 0.5 + dest_loc_x = str(round(float(src_loc_x) + (vector_length - 30.) / vector_length * delta_x, 2)) + dest_loc_y = str(round(float(src_loc_y) + (vector_length - 30.) / vector_length * delta_y, 2)) + + # Creating nodes + g_node = xml_map.createElement('g') + + path_with_shadow = xml_map.createElement('path') + path_with_shadow.setAttribute('class', 'shadowdash') + path_with_shadow.setAttribute('d', 'M {},{} C {},{} {},{} {},{}'.format(loc_x, loc_y, + src_loc_x, src_loc_y, + src_loc_x, src_loc_y, + dest_loc_x, dest_loc_y)) + + path_with_arrow = xml_map.createElement('path') + path_with_arrow.setAttribute('class', 'supportorder') + path_with_arrow.setAttribute('stroke', self.metadata['color'][power_name]) + path_with_arrow.setAttribute('marker-end', 'url(#arrow)') + path_with_arrow.setAttribute('d', 'M {},{} C {},{} {},{} {},{}'.format(loc_x, loc_y, + src_loc_x, src_loc_y, + src_loc_x, src_loc_y, + dest_loc_x, dest_loc_y)) + + # Inserting + g_node.appendChild(path_with_shadow) + g_node.appendChild(path_with_arrow) + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'OrderLayer': + for layer_node in child_node.childNodes: + if layer_node.nodeName == 'g' and _attr(layer_node, 'id') == 'Layer2': + layer_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + 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') + :param dest_loc: The location where the unit being convoyed is moving to (e.g. 'MAR') + :param power_name: The power name issuing the convoy order + :return: Nothing + """ + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], 10) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], 10) + src_loc_x = _offset(self.metadata['coord'][src_loc]['unit'][0], 10) + src_loc_y = _offset(self.metadata['coord'][src_loc]['unit'][1], 10) + dest_loc_x = _offset(self.metadata['coord'][dest_loc]['unit'][0], 10) + dest_loc_y = _offset(self.metadata['coord'][dest_loc]['unit'][1], 10) + + # Adjusting starting arrow (from convoy to start location) + # This is to avoid the end of the arrow conflicting with the convoy triangle + src_delta_x = float(src_loc_x) - float(loc_x) + src_delta_y = float(src_loc_y) - float(loc_y) + src_vector_length = (src_delta_x ** 2. + src_delta_y ** 2.) ** 0.5 + src_loc_x_1 = str(round(float(loc_x) + (src_vector_length - 30.) / src_vector_length * src_delta_x, 2)) + src_loc_y_1 = str(round(float(loc_y) + (src_vector_length - 30.) / src_vector_length * src_delta_y, 2)) + + # Adjusting destination arrow (from start location to destination location) + # This is to avoid the start of the arrow conflicting with the convoy triangle + dest_delta_x = float(src_loc_x) - float(dest_loc_x) + dest_delta_y = float(src_loc_y) - float(dest_loc_y) + dest_vector_length = (dest_delta_x ** 2. + dest_delta_y ** 2.) ** 0.5 + src_loc_x_2 = str(round(float(dest_loc_x) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_x, 2)) + src_loc_y_2 = str(round(float(dest_loc_y) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_y, 2)) + + # Adjusting destination arrow (from start location to destination location) + # This is to avoid the start of the arrow conflicting with the convoy triangle + dest_delta_x = float(dest_loc_x) - float(src_loc_x) + dest_delta_y = float(dest_loc_y) - float(src_loc_y) + dest_vector_length = (dest_delta_x ** 2. + dest_delta_y ** 2.) ** 0.5 + dest_loc_x = str(round(float(src_loc_x) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_x, 2)) + dest_loc_y = str(round(float(src_loc_y) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_y, 2)) + + # Getting convoy triangle coordinates + triangle_coord = [] + triangle_loc_x = _offset(self.metadata['coord'][src_loc]['unit'][0], 10) + triangle_loc_y = _offset(self.metadata['coord'][src_loc]['unit'][1], 10) + for offset in [(0, -38.3), (33.2, 19.1), (-33.2, 19.1)]: + triangle_coord += [_offset(triangle_loc_x, offset[0]) + ',' + _offset(triangle_loc_y, offset[1])] + + # Creating nodes + g_node = xml_map.createElement('g') + + src_shadow_line = xml_map.createElement('line') + src_shadow_line.setAttribute('x1', loc_x) + src_shadow_line.setAttribute('y1', loc_y) + src_shadow_line.setAttribute('x2', src_loc_x_1) + src_shadow_line.setAttribute('y2', src_loc_y_1) + src_shadow_line.setAttribute('class', 'shadowdash') + + dest_shadow_line = xml_map.createElement('line') + dest_shadow_line.setAttribute('x1', src_loc_x_2) + dest_shadow_line.setAttribute('y1', src_loc_y_2) + dest_shadow_line.setAttribute('x2', dest_loc_x) + dest_shadow_line.setAttribute('y2', dest_loc_y) + dest_shadow_line.setAttribute('class', 'shadowdash') + + src_convoy_line = xml_map.createElement('line') + src_convoy_line.setAttribute('x1', loc_x) + src_convoy_line.setAttribute('y1', loc_y) + src_convoy_line.setAttribute('x2', src_loc_x_1) + src_convoy_line.setAttribute('y2', src_loc_y_1) + src_convoy_line.setAttribute('class', 'convoyorder') + src_convoy_line.setAttribute('stroke', self.metadata['color'][power_name]) + + dest_convoy_line = xml_map.createElement('line') + dest_convoy_line.setAttribute('x1', src_loc_x_2) + dest_convoy_line.setAttribute('y1', src_loc_y_2) + dest_convoy_line.setAttribute('x2', dest_loc_x) + dest_convoy_line.setAttribute('y2', dest_loc_y) + dest_convoy_line.setAttribute('class', 'convoyorder') + dest_convoy_line.setAttribute('stroke', self.metadata['color'][power_name]) + dest_convoy_line.setAttribute('marker-end', 'url(#arrow)') + + shadow_poly = xml_map.createElement('polygon') + shadow_poly.setAttribute('class', 'shadowdash') + shadow_poly.setAttribute('points', ' '.join(triangle_coord)) + + convoy_poly = xml_map.createElement('polygon') + convoy_poly.setAttribute('class', 'convoyorder') + convoy_poly.setAttribute('points', ' '.join(triangle_coord)) + convoy_poly.setAttribute('stroke', self.metadata['color'][power_name]) + + # Inserting + g_node.appendChild(src_shadow_line) + g_node.appendChild(dest_shadow_line) + g_node.appendChild(src_convoy_line) + g_node.appendChild(dest_convoy_line) + g_node.appendChild(shadow_poly) + g_node.appendChild(convoy_poly) + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'OrderLayer': + for layer_node in child_node.childNodes: + if layer_node.nodeName == 'g' and _attr(layer_node, 'id') == 'Layer2': + layer_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + 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') + :param power_name: The name of the power building the unit + :return: Nothing + """ + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], -11.5) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], - 10.) + build_loc_x = _offset(self.metadata['coord'][loc]['unit'][0], -20.5) + build_loc_y = _offset(self.metadata['coord'][loc]['unit'][1], -20.5) + + # Symbols + symbol = ARMY if unit_type == 'A' else FLEET + build_symbol = 'BuildUnit' + + # Creating nodes + g_node = xml_map.createElement('g') + + symbol_node = xml_map.createElement('use') + symbol_node.setAttribute('x', loc_x) + symbol_node.setAttribute('y', loc_y) + symbol_node.setAttribute('height', self.metadata['symbol_size'][symbol][0]) + symbol_node.setAttribute('width', self.metadata['symbol_size'][symbol][1]) + symbol_node.setAttribute('xlink:href', '#{}'.format(symbol)) + symbol_node.setAttribute('class', 'unit{}'.format(power_name.lower())) + + build_node = xml_map.createElement('use') + build_node.setAttribute('x', build_loc_x) + build_node.setAttribute('y', build_loc_y) + build_node.setAttribute('height', self.metadata['symbol_size'][build_symbol][0]) + build_node.setAttribute('width', self.metadata['symbol_size'][build_symbol][1]) + build_node.setAttribute('xlink:href', '#{}'.format(build_symbol)) + + # Inserting + g_node.appendChild(build_node) + g_node.appendChild(symbol_node) + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'HighestOrderLayer': + child_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + 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 + """ + if self.game.get_current_phase()[-1] == 'R': + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], -29.) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], -27.5) + else: + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], -16.5) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], -15.) + + # Symbols + symbol = 'RemoveUnit' + + # Creating nodes + g_node = xml_map.createElement('g') + symbol_node = xml_map.createElement('use') + symbol_node.setAttribute('x', loc_x) + symbol_node.setAttribute('y', loc_y) + symbol_node.setAttribute('height', self.metadata['symbol_size'][symbol][0]) + symbol_node.setAttribute('width', self.metadata['symbol_size'][symbol][1]) + symbol_node.setAttribute('xlink:href', '#{}'.format(symbol)) + + # Inserting + g_node.appendChild(symbol_node) + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'HighestOrderLayer': + child_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map diff --git a/diplomacy/maps/__init__.py b/diplomacy/maps/__init__.py new file mode 100644 index 0000000..4f2769f --- /dev/null +++ b/diplomacy/maps/__init__.py @@ -0,0 +1,16 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== diff --git a/diplomacy/maps/ancmed.map b/diplomacy/maps/ancmed.map new file mode 100644 index 0000000..cafce6b --- /dev/null +++ b/diplomacy/maps/ancmed.map @@ -0,0 +1,190 @@ +BEGIN SPRING 1 MOVEMENT + +CARTHAGE (CARTHAGINIAN) CAR CIR THA +F THA +A CAR +A CIR + +EGYPT (EGYPTIAN) ALE MEM THB +F ALE +A MEM +A THB + +GREECE (GREEK) SPA ATH MAC +F SPA +A ATH +A MAC + +PERSIA (PERSIAN) ANT SID DAM +F SID +A ANT +A DAM + +ROME (ROMAN) NEA ROM RAV +F NEA +A ROM +A RAV + +UNOWNED SAG MAS BAL SAD SIC VIN DAL +UNOWNED NUM LEP CYR CRE CYP PET +UNOWNED JER TYE SIP CHE BYZ MIL + +Adriatic Sea = ADR +Aegean Sea = AEG +Alexandria = ALE +Antioch = ANT +Apulia = APU +Arabia = ARA +Armenia = ARM +Athens = ATH +Ausonian Sea = AUS +Baleares = BAL +Bayuda = BAY +Berber Sea = BER +Bithynia = BIT +Black Sea = BLA +Byzantium = BYZ +Cappadocia = CAP +Province of Carthage = CAR Carthago +Chersonesus = CHE +Cilician Strait = CIL +Cirta = CIR +Corsica = COR +Crete = CRE +Cyprus = CYP +Cyrene = CYR +Dacia = DAC +Dalmatia = DAL +Damascus = DAM +Egyptian Sea = EGY +Epirus = EPI +Etruria = ETR +Galatia = GAL +Gaul = GAU +Gulf of Pelusium = GOP +Gulf of Syrtis = GOS +Gulf of Tacape = GOT +Iberian Sea = IBE +Illyria = ILL +Ionian Sea = ION +Isauria = ISA +Jerusalem = JER +Leptis = LEP +Libyan Sea = LIB +Ligurian Sea = LIG +Lusitania = LUS +Macedonia = MAC +Marmarica = MAR +Massilia = MAS +Mauretania = MAU +Memphis = MEM +Messenian Sea = MES +Miletus = MIL +Minoan Sea = MIN +Nabatea = NAB +Neapolis = NEA +Numidia = NUM +Petra = PET +Phazania = PHA +Punic Sea = PUN +Ravenna = RAV +Red Sea = REE reed+sea +Rhaetia = RHA +Roma = ROM +Saguntum = SAG +Sahara = SAH +Sardinia = SAD +Sarmatia = SAM +Sicilia = SIC +Sidon = SID +Sinai = SIN SII +Sinope = SIP +Sparta = SPA +Syrian Sea = SYR +Tarraconensis = TAR +Thapsus = THA +Thebes = THB +Tyre = TYE +Tyrrhenean Sea = TYN +Venetia = VEN +Vindobona = VIN + +WATER ADR ABUTS ION APU RAV VEN DAL EPI +WATER AEG ABUTS SPA MAC BYZ MIL CRE MIN MES ATH +COAST ALE ABUTS CYR SIN GOP EGY MEM THB LIB +COAST ANT ABUTS CAP SID CIL DAM +COAST APU ABUTS ADR RAV NEA ION rom +LAND ARA ABUTS DAM NAB JER SID TYE +LAND ARM ABUTS CHE SIP DAM CAP +COAST ATH ABUTS SPA MAC EPI AEG ION +WATER AUS ABUTS TYN NEA SIC ION MES LIB GOT PUN +PORT BAL ABUTS LIG BER Tar Sag +COAST BAY ABUTS THB MEM PHA MAR SAH +WATER BER ABUTS IBE LIG PUN SAD BAL SAG MAU CAR +COAST BIT ABUTS BYZ SIP BLA GAL +WATER BLA ABUTS BYZ DAC CHE SIP BIT +COAST BYZ ABUTS BLA AEG BIT MIL GAL DAC MAC +COAST CAP ABUTS ISA ANT CIL sip GAL ARM DAM +COAST CAR ABUTS BER PUN MAU THA CIR +COAST CHE ABUTS BLA DAC SIP SAM ARM +WATER CIL ABUTS CYP SYR EGY MIN MIL ISA CAP ANT SID +LAND CIR ABUTS CAR MAU THA NUM SAH PHA +COAST COR ABUTS SAD LIG TYN +COAST CRE ABUTS AEG MIN EGY LIB MES +COAST CYP ABUTS SYR CIL EGY +COAST CYR ABUTS LEP ALE MAR mem LIB GOS +COAST DAC ABUTS SAM ILL mac BLA BYZ CHE +COAST DAL ABUTS VEN ADR EPI VIN ILL +LAND DAM ABUTS ARM CAP ANT SID ARA +WATER EGY ABUTS ALE LIB CRE CYP MIN CIL GOP SYR +COAST EPI ABUTS DAL ADR ION mac ILL ATH +COAST ETR ABUTS ven rav RHA MAS LIG ROM +LAND GAL ABUTS BYZ MIL BIT ISA SIP CAP +LAND GAU ABUTS LUS TAR MAS RHA +WATER GOP ABUTS JER SIN ALE EGY SYR THB +WATER GOS ABUTS GOT LIB CYR LEP +WATER GOT ABUTS GOS NUM LEP THA LIB AUS MES PUN +WATER IBE ABUTS SAG MAU BER +LAND ILL ABUTS EPI MAC DAC SAM VIN DAL +WATER ION ABUTS ADR EPI SPA MES AUS NEA APU ATH +COAST ISA ABUTS CAP MIL CIL GAL +COAST JER ABUTS TYE SIN pet ARA nab SYR GOP +COAST LEP ABUTS CYR NUM GOS GOT MAR PHA +WATER LIB ABUTS MES AUS GOT GOS CYR EGY CRE ALE +WATER LIG ABUTS BER BAL TAR MAS ETR ROM TYN COR SAD +LAND LUS ABUTS SAG TAR GAU +COAST MAC ABUTS AEG BYZ dac ILL epi ATH +LAND MAR ABUTS CYR MEM BAY PHA LEP +COAST MAS ABUTS TAR GAU RHA ETR LIG +COAST MAU ABUTS SAG IBE BER CAR CIR SAH +COAST MEM ABUTS BAY ALE THB MAR cyr +WATER MES ABUTS SPA ION AUS GOT LIB CRE AEG +COAST MIL ABUTS BYZ AEG ISA MIN CIL GAL +WATER MIN ABUTS MIL AEG CRE EGY CIL +COAST NAB ABUTS PET REE ARA jer +COAST NEA ABUTS APU ROM SIC AUS TYN ION +COAST NUM ABUTS LEP THA GOT PHA CIR +COAST PET ABUTS NAB jer SIN REE +LAND PHA ABUTS SAH CIR NUM LEP MAR BAY +WATER PUN ABUTS CAR SAD TYN SIC BER AUS GOT THA +COAST RAV ABUTS APU VEN ADR rom etr +WATER REE ABUTS NAB PET SIN THB +LAND RHA ABUTS GAU MAS ETR VEN SAM VIN +COAST ROM ABUTS ETR NEA LIG TYN rav apu +COAST SAG ABUTS MAU TAR IBE Bal BER LUS +LAND SAH ABUTS MAU PHA BAY CIR +COAST SAD ABUTS COR PUN TYN BER LIG +LAND SAM ABUTS CHE DAC ILL VIN RHA +COAST SIC ABUTS NEA TYN AUS PUN +COAST SID ABUTS TYE ANT DAM ARA SYR CIL +COAST SIN ABUTS GOP ALE JER THB PET REE +COAST SIP ABUTS BIT BLA CHE ARM cap GAL +COAST SPA ABUTS AEG ION MES ATH +WATER SYR ABUTS CIL CYP EGY GOP JER TYE SID +COAST TAR ABUTS SAG MAS LUS GAU Bal LIG +COAST THA ABUTS CAR NUM CIR GOT PUN +COAST THB ABUTS BAY MEM ALE SIN GOP REE +COAST TYE ABUTS SID JER SYR ARA +WATER TYN ABUTS NEA SIC AUS PUN SAD COR LIG ROM +COAST VEN ABUTS RAV ADR DAL VIN RHA etr +LAND VIN ABUTS RHA VEN ILL SAM DAL diff --git a/diplomacy/maps/colonial.map b/diplomacy/maps/colonial.map new file mode 100644 index 0000000..1b46a26 --- /dev/null +++ b/diplomacy/maps/colonial.map @@ -0,0 +1,299 @@ +BEGIN SPRING 1870 MOVEMENT + +BRITAIN (BRITISH) ADE BOM DEL HON MAD SIN +A MAD +A DEL +F BOM +F HON +F SIN +F ADE + +CHINA (CHINESE) CAN MAC PEK SHA SIK +A PEK +A CAN +A SIK +A SHA +A MAC + +FRANCE (FRENCH) ANN COC TON +A TON +F ANN +A COC + +HOLLAND (DUTCH:H) BOR JAV SUM +F JAV +A BOR +F SUM + +JAPAN (JAPANESE) KYO KYU OTA TOK +A KYO +F KYU +F OTA +F TOK + +RUSSIA (RUSSIAN) MOS ODE OMS POR VLA +A MOS +A VLA +A OMS +F ODE +F POR + +TURKEY (TURKISH) ANG BAG CON +A ANG +F BAG +F CON + +UNOWNED ASS BAN BEN BUR CEB CEY CHU DAV EGY FOR FUS KAG KAM KAR MAL MAY MNA MON NEW PER RAN RUM SAK SAR SEO SHI SUD TAB TAS + + +Abysinnia = ABY +Aden = ADE +Afghanistan = AFG +Akita = AKI +Akmolinsk = AKM +Andaman Sea = AND +Angora = ANG +Annam = ANN +Arabia = ARB +Arabia (North Coast) = ARB/NC +Arabia (South Coast) = ARB/SC +Armenia = ARM +Arabian Sea = ARS +Assam = ASS +Baghdad = BAG +Baku = BAK +Bangkok = BAN +Bangkok (East Coast) = BAN/EC +Bangkok (West Coast) = BAN/WC +Bengal = BEN +Black Sea = BLA +Bay of Bengal = BOB +Bokhara = BOK +Bombay = BOM +Borneo = BOR +Upper Burma = BUR +Cambodia = CAM +Canton = CAN +Cebu = CEB +Celebes = CEL +Ceylon = CEY +Chungking = CHU +Celebes Sea = CLS +Cochin = COC +Constantinople = CON +Davao = DAV +Delhi = DEL +East China Sea = ECS +Egypt = EGY +East Indian Ocean = EIO +Eritrea = ERI +Formosa = FOR +Fusan = FUS +Gulf of Aden = GOA +Gulf of Manaar = GOM +Gulf of Siam = GOS +Hong Kong = HON +Hyderabad = HYD +Irkutsk = IRK +Java = JAV +Java Sea = JVS +Kashgar = KAG +Kashmir = KAM +Karachi = KAR +Kirghiz = KIR +Krasnoyarsk = KRA +Kyoto = KYO +Kyushu = KYU +Langchow = LAN +Lower Pacific = LOW +Lucknow = LUC +Luzon Strait = LUZ +Manchuria = MAC +Madras = MAD +Malaya = MAL +Mandalay = MAY +Mecca = MEC +Mediterranean Sea = MED +Middle Pacific = MID +Manila = MNA +Mongolia = MON +Moscow = MOS +Mysore = MYS +Nagpur = NAG +Nanchung = NAN +Nepal = NEP +New Guinea = NEW +Odessa = ODE +Okhotsk Sea = OKH +Oman = OMA +Omsk = OMS +Orenburg = ORE +Otaru = OTA +Peking = PEK +Persia = PER +Persian Gulf = PGF +Port Arthur = POR +Perm = PRM +Punjab = PUN +Rajputana = RAJ +Rangoon = RAN +Red Sea = RED +Rumania = RUM +Sakhalin = SAK +Sarawak = SAR +South China Sea = SCS +Semipalatinsk = SEM +Seoul = SEO +Seoul (East Coast) = SEO/EC +Seoul (West Coast) = SEO/WC +Shanghai = SHA +Shiraz = SHI +North Siam = SIA +Sinkiang = SIK +Singapore = SIN +Southeast Indian Ocean = SIO +Sea of Japan = SOJ +Somalia = SOM +Sudan = SUD +Sulu Sea = SUL +Sumatra = SUM +Sunda Sea = SUN +Syria = SYR +Tabriz = TAB +Tashkent = TAS +Tibet = TIB +Timor Sea = TIM +Tokyo = TOK +Tongking = TON +Upper Pacific = UPP +Urumchi = URU +Vladivostok = VLA +West Indian Ocean = WIO +Yellow Sea = YEL +Yunnan = YUN + +LAND ABY ABUTS ERI SOM SUD +COAST ADE ABUTS ARB/SC GOA MEC RED +LAND AFG ABUTS BOK KAG KAM KAR PER PUN TAS +COAST AKI ABUTS KYO OKH OTA SOJ +LAND AKM ABUTS KIR KRA OMS ORE SEM TAS +WATER AND ABUTS BAN/WC BOB EIO GOM JVS MAL RAN SUM +COAST ANG ABUTS ARM BLA CON MED SYR +COAST ANN ABUTS cam COC GOS SCS TON +COAST arb ABUTS ADE ARS BAG GOA MEC OMA PGF SYR +COAST ARB/NC ABUTS BAG OMA PGF +COAST ARB/SC ABUTS ADE ARS GOA OMA +COAST ARM ABUTS ANG bag BAK BLA syr TAB +WATER ARS ABUTS ARB/SC BOM GOA KAR MYS OMA PGF RAJ WIO +LAND ASS ABUTS BEN BUR SIK TIB YUN +COAST BAG ABUTS ARB/NC arm PGF SHI syr TAB +COAST BAK ABUTS ARM BLA MOS ODE TAB +COAST ban ABUTS AND CAM GOS MAL RAN SIA +COAST BAN/EC ABUTS CAM GOS MAL +COAST BAN/WC ABUTS AND MAL RAN +COAST BEN ABUTS ASS BOB BUR HYD LUC NEP TIB +WATER BLA ABUTS ANG ARM BAK MED ODE RUM +WATER BOB ABUTS AND BEN BUR GOM HYD RAN +LAND BOK ABUTS AFG MOS ORE PER TAS +COAST BOM ABUTS ARS hyd MYS NAG RAJ +COAST BOR ABUTS CLS JVS SAR +COAST BUR ABUTS ASS BEN BOB MAY RAN YUN +COAST CAM ABUTS ann BAN/EC COC GOS SIA ton +COAST CAN ABUTS CHU HON MAY NAN SCS TON YUN +COAST CEB ABUTS DAV LOW LUZ MID MNA SUL +COAST CEL ABUTS CLS JVS TIM +COAST CEY ABUTS EIO GOM WIO +LAND CHU ABUTS CAN LAN NAN SIK YUN +WATER CLS ABUTS BOR CEL DAV JVS LOW NEW SAR SUL TIM +COAST COC ABUTS ANN CAM GOS +COAST CON ABUTS ANG MED RUM +COAST DAV ABUTS CEB CLS LOW MID SUL +LAND DEL ABUTS LUC NAG NEP PUN RAJ +WATER ECS ABUTS FOR KYU NAN SCS SHA UPP YEL +COAST EGY ABUTS MEC MED RED SUD SYR +WATER EIO ABUTS AND CEY GOM SIO SUM WIO +COAST ERI ABUTS ABY GOA RED SOM SUD +COAST FOR ABUTS ECS LUZ MID SCS UPP +COAST FUS ABUTS SEO/EC SEO/WC SOJ YEL +WATER GOA ABUTS ADE ARB/SC ARS ERI RED SOM WIO +WATER GOM ABUTS AND BOB CEY EIO HYD MAD WIO +WATER GOS ABUTS ANN BAN/EC CAM COC MAL SCS SUN +COAST HON ABUTS CAN SCS +COAST HYD ABUTS BEN BOB bom GOM LUC MAD mys NAG +LAND IRK ABUTS KRA MAC MON VLA +COAST JAV ABUTS JVS SIO TIM +WATER JVS ABUTS AND BOR CEL CLS JAV MAL SAR SIN SIO SUM SUN TIM +LAND KAG ABUTS AFG KAM KIR SIK TAS TIB URU +LAND KAM ABUTS AFG KAG PUN TIB +COAST KAR ABUTS AFG ARS PER PGF PUN RAJ +LAND KIR ABUTS AKM KAG SEM TAS URU +LAND KRA ABUTS AKM IRK MON OMS SEM URU +COAST KYO ABUTS AKI KYU SOJ TOK UPP YEL +COAST KYU ABUTS ECS KYO UPP YEL +LAND LAN ABUTS CHU MON NAN PEK SHA SIK +WATER LOW ABUTS CEB CLS DAV MID NEW +LAND LUC ABUTS BEN DEL HYD NAG NEP +WATER LUZ ABUTS CEB FOR MID MNA SCS SUL +COAST MAC ABUTS IRK MON PEK POR seo SHA vla YEL +COAST MAD ABUTS GOM HYD MYS WIO +COAST MAL ABUTS AND BAN/EC BAN/WC GOS JVS SIN SUN +LAND MAY ABUTS BUR CAN RAN SIA TON YUN +COAST MEC ABUTS ADE arb EGY RED syr +WATER MED ABUTS ANG BLA CON EGY SYR +WATER MID ABUTS CEB DAV FOR LOW LUZ MNA UPP +COAST MNA ABUTS CEB LUZ MID +LAND MON ABUTS IRK KRA LAN MAC PEK SIK URU +LAND MOS ABUTS BAK BOK ODE ORE PRM +COAST MYS ABUTS ARS BOM hyd MAD WIO +LAND NAG ABUTS BOM DEL HYD LUC RAJ +COAST NAN ABUTS CAN CHU ECS LAN SCS SHA +LAND NEP ABUTS BEN DEL LUC PUN +COAST NEW ABUTS CLS LOW TIM +COAST ODE ABUTS BAK BLA MOS RUM +WATER OKH ABUTS AKI OTA SAK SOJ TOK UPP VLA +COAST OMA ABUTS ARB/NC ARB/SC ARS PGF +LAND OMS ABUTS AKM KRA ORE PRM +LAND ORE ABUTS AKM BOK MOS OMS PRM TAS +COAST OTA ABUTS AKI OKH SAK SOJ +LAND PEK ABUTS LAN MAC MON SHA +COAST PER ABUTS AFG BOK KAR PGF SHI TAB +WATER PGF ABUTS ARB/NC ARS BAG KAR OMA PER SHI +COAST POR ABUTS MAC SEO/WC YEL +LAND PRM ABUTS MOS OMS ORE +LAND PUN ABUTS AFG DEL KAM KAR NEP RAJ TIB +COAST RAJ ABUTS ARS BOM DEL KAR NAG PUN +COAST RAN ABUTS AND BAN/WC BOB BUR MAY SIA +WATER RED ABUTS ADE EGY ERI GOA MEC SUD +COAST RUM ABUTS BLA CON ODE +COAST SAK ABUTS OKH OTA +COAST SAR ABUTS BOR CLS JVS SUL SUN +WATER SCS ABUTS ANN CAN ECS FOR GOS HON LUZ NAN SUL SUN TON +LAND SEM ABUTS AKM KIR KRA URU +COAST seo ABUTS FUS MAC POR SOJ VLA YEL +COAST SEO/EC ABUTS FUS SOJ VLA +COAST SEO/WC ABUTS FUS POR YEL +COAST SHA ABUTS ECS LAN MAC NAN PEK YEL +COAST SHI ABUTS BAG PER PGF TAB +LAND SIA ABUTS BAN CAM MAY RAN TON +LAND SIK ABUTS ASS CHU KAG LAN MON TIB URU YUN +COAST SIN ABUTS JVS MAL +WATER SIO ABUTS EIO JAV JVS SUM TIM WIO +WATER SOJ ABUTS AKI FUS KYO OKH OTA SEO/EC VLA YEL +COAST SOM ABUTS ABY ERI GOA +COAST SUD ABUTS ABY EGY ERI RED +WATER SUL ABUTS CEB CLS DAV LUZ SAR SCS SUN +COAST SUM ABUTS AND EIO JVS SIO +WATER SUN ABUTS GOS JVS MAL SAR SCS SUL +COAST SYR ABUTS ANG arb arm bag EGY mec MED +LAND TAB ABUTS ARM BAG BAK PER SHI +LAND TAS ABUTS AFG AKM BOK KAG KIR ORE +LAND TIB ABUTS ASS BEN KAG KAM PUN SIK +WATER TIM ABUTS CEL CLS JAV JVS NEW SIO +COAST TOK ABUTS KYO OKH UPP +COAST TON ABUTS ANN cam CAN MAY SCS SIA +WATER UPP ABUTS ECS FOR KYO KYU MID OKH TOK YEL +LAND URU ABUTS KAG KIR KRA MON SEM SIK +COAST VLA ABUTS IRK mac OKH SEO/EC SOJ +WATER WIO ABUTS ARS CEY EIO GOA GOM MAD MYS SIO +WATER YEL ABUTS ECS FUS KYO KYU MAC POR SEO/WC SHA SOJ UPP +LAND YUN ABUTS ASS BUR CAN CHU MAY SIK diff --git a/diplomacy/maps/empire.map b/diplomacy/maps/empire.map new file mode 100644 index 0000000..63073d4 --- /dev/null +++ b/diplomacy/maps/empire.map @@ -0,0 +1,410 @@ +BEGIN SPRING 1999 MOVEMENT + +BRITISH-COLUMBIA (British+Columbian) ANC CGY VAN +A VAN +A CGY +F ANC + +CALIFORNIA (Californian) SFR LAN SDI +A SFR +F SDI +A LAN + +FLORIDA (Floridian) MIA JAC TAM +F MIA +A JAC +A TAM + +HEARTLAND (Heartlander) CHI MIN MIL +A CHI +A MIN +A MIL + +NEW-YORK (Yankee) NYC NJE PHI +F NYC +A NJE +A PHI + +PERU (Peruvian) BOG CAL LIM +A BOG +F CAL/NC +F LIM + +QUEBEC (Quebecois) UNG QUE MON +A MON +F QUE +F UNG + +TEXAS (Texan) HOU SAN DAL +F HOU +A DAL +A SAN + +CUBA (Cuban:U) HAV HOL KIN +F HAV +A HOL +F KIN + +MEXICO (Mexican:X) GUA VER MEX +A MEX +F GUA +A VER + +UNOWNED MAN ONT NSC WAS ORE ARI COL KAN MIS LOU MIC OHI MAS GRE +UNOWNED WDC TEN GEO NCA DOM CHH NLE DUR GUT YUC NIC PAN VEN HAW + +Anchorage = anc ak alaska +Yukon = yuk +North West Territories = nwt northwest +Nunavut = nun +Manitoba = man mb +Saskatchewan = sas sask sk +Calgary = cgy calg +Northern BC = nbc +Vancouver = van +Northern Ontario = non nont +Western Ontario = won wont +Ontario = ont sont +Abitibi = abi +Ungava = ung +Labrador = lab +Cote-Nord = cot cotn cn cnord cote +Quebec City = que qbc qc province+of+quebec +Montreal = mon montr mtl +Beauce = bea beau +Gaspesie = gas gasp gaspe +New Brunswick (north coast) = nbr/nc brunswick/nc bru/nc nbrunswick/nc +New Brunswick (south coast) = nbr/sc brunswick/sc bru/sc nbrunswick/sc +New Brunswick = nbr brunswick bru nbrunswick +Nova Scotia = nsc nov nscotia ns +Newfoundland = new newf nf nfl +Washington = was wa +Oregon = ore or +Idaho = ida id +San Francisco = sfr frisco cisco sanf saf sfrancisco sf chinatown +Los Angeles = lan los langeles la hollywood burbank lax +San Diego = sdi diego sdiego sand sd sandyeggo +Arizona = ari az +Utah = uta ut slc +Nevada = nev nv vegas reno tahoe +New Mexico = nme nmex nm nmexico +Wyoming = wyo wy godscountry +Colorado = col co den denver justsouthofheaven +Montana = mta monta mt +Dallas = dal +Oklahoma = okl ok +Kansas = kan ks +Nebraska = neb nb +Dakotas = dak +Minneapolis = min mn +Iowa = iow ia +Missouri = mis mo +Arkansas = ark ar +Louisiana = lou +Houston = hou +San Antonio = san sana santonio sa +West Texas = wte wtex wtx wt +Lake Ontario = lon lont lontario +Upper Peninsula = upe upp upen up +Michigan = mic mich mi +Indiana = ind indy +Chicago = chi il illinois windycity mykindatown +Lake Michigan = lmi lmic lmichigan +Lake Superior = lsu lsup lsuperior superior +Lake Huron = lhu lhur lhuron huron +Lake Erie = ler lerie leri erie +Milwaukee (north coast) = mil/nc milw/nc wi/nc +Milwaukee (east coast) = mil/ec milw/ec wi/ec +Milwaukee = mil milw wi +Ohio = ohi oh +West Pennsylvania = wpe wpen wpa +New York State = nys upstate upstateny +Maine = mai main me +Vermont = vem verm vt +Massachusetts = mas mass ma +New York City = nyc manhattan ny york bigapple bronx queens brooklyn +New Jersey = nje njer nj njtp gsp jersey joisey +Washington D+C = wdc dc georgetown washington+dc +Philadelphia = phi phil philly epa +Virginia = vir va +West Virginia = wvi wvir wv wva +Kentucky = ken ky +Tennessee = ten tn +Deep South = dso dsou ds al ms dsouth deep dee +Georgia = geo ga +South Carolina = sca scaro scarolina +North Carolina = nca ncaro ncarolina +Florida Panhandle = fpa fpan panhandle +Jacksonville = jac +Miami = mia ftl ftlauderdale +Tampa = tam stpete stp +Havana = hav +Camaguey (north coast) = cam/nc +Camaguey (south coast) = cam/sc +Camaguey = cam +Holguin = hol +Haiti = hai +Dominican Republic = dom domr drepublic dominican drep +Kingston = kin king kings jam jamaica +Baja California = baj baja bcal bcalifornia bca +Chihuahua = chh chih +Coahuila = coa +Nuevo Leon = nle nuevo leon nleo nleon +Durango = dur +Guadalajara = gua guad +Potosi = pot +Veracruz = ver +Mexico City = mex province+of+mexico +Guerrero = gue +Oaxaca = oax +Chiapas = chp chia +Tabasco = tab +Guatemala (south coast) = gut/sc guat/sc +Guatemala (east coast) = gut/ec guat/ec +Guatemala = gut guat +Yucatan = yuc +El Salvador = esa salvador esal elsa elsal es els +Honduras (north coast) = hon/nc +Honduras (south coast) = hon/sc +Honduras = hon +Nicaragua (east coast) = nic/ec +Nicaragua (west coast) = nic/wc +Nicaragua = nic +Costa Rica (north coast) = cos/nc cos/nc cric/nc costa/nc rica/nc cr/nc +Costa Rica (south coast) = cos/sc cos/sc cric/sc costa/sc rica/sc cr/sc +Costa Rica = cos cos cric costa rica cr +Panama = pan +Cali (north coast) = cal/nc +Cali (south coast) = cal/sc +Cali = cal +Antioquia = ant +Guajira = guj guaj +Venezuela = ven +Vichada = vic +Bogota = bog +Lima = lim +Ecuador = ecu +Hawaii = haw oahu honolulu hi +Greenland = gre +Bering Sea = ber bers bering +Arctic Ocean = aro arco arctic arc ao +Beaufort Sea = bef befs beaufort +Hudson Bay = hub hudb hudson hud hb +Baffin Bay = bab bafb baffin baf +Sea of Labrador = sol sola slab +North Atlantic Ocean = nao nat natlantic +Gulf of St-Lawrence = gsl gstl gsla lawrence +Massachusetts Bay = mab masb mba mbay +Mid Atlantic Ocean = mao mat matlantic mid +Chesapeake Bay = chb chesapeake cheb +Cape May = cma may cmay +Sea of Sargasso = sos sargasso sosa +Bermuda Triangle = bet bermuda triangle bert tri +Eastern Seaboard = esb eseab eastcoast eco ecoast ecoa east+coast +Straits of Florida = sof sflorida sflor +East Caribbean Sea = ecs ecars ecaribbean +Lesser Antilles = les lesser lantilles antilles lant +Apalachee Bay = apb apalachee apab apa +Gulf of Mexico = gom gmexico gmex gomex gm +Gulf of Campeche = goc campeche gcam +Straits of Yucatan = soy syuc syucatan sy +Gulf of Honduras = goh ghonduras ghon gh +Cayman Trench = cat cay cayman trench cayt ctr +West Caribbean Sea = wcs wcars wcaribbean +South Caribbean Sea = scs scars scaribbean +Gulf of Mosquitos = gmo gomos gmos mosq mosquitos gmosquitos gomo +North Pacific Ocean = npo npa npac npacific +Gulf of Alaska = goa galaska goal +Queen Charlotte Sound = qcs queen charlotte sound qsound qcsound +Straits of Juan de Fuca = sjf juan fuca sjuan sfuca +Western Seaboard = wsb wseab westcoast wco wcoast wcoa west+coast +Mid Pacific Ocean = mpo mpa mpac mpacific +Gulf of Santa Catalina = gsc catalina gsca +South West Pacific Ocean = swp swpa swpo swpac swpacific +Coast of Mexico = com cmexico cmex cm +Gulf of California = gca gcal gcalifornia +Gulf of Tehuantepec = got tehuantepec gteh gt +South Pacific Ocean = spo spa spac spacific +Galapagos = gal turtle +Gulf of Guayaquil = gog guayaquil ggua gg +Gulf of Panama = gop gpanama gpan gp +Coronado Bay = cob coronado corb cb +Gulf of Fonseca = gof fonseca gfon gf + +COAST ANC ABUTS YUK VAN NBC ARO BEF QCS GOA BER +COAST YUK ABUTS BEF NWT ANC NBC +COAST NWT ABUTS BEF NUN YUK ARO SAS CGY NBC +COAST NUN ABUTS ARO BAB BEF SOL HUB MAN NWT UNG SAS +COAST MAN ABUTS NUN HUB NON won min DAK SAS +LAND SAS ABUTS NWT NUN MAN DAK MTA CGY +LAND CGY ABUTS NWT SAS MTA VAN NBC +LAND NBC ABUTS YUK NWT CGY VAN ANC +COAST VAN ABUTS ANC WAS SJF QCS NBC CGY MTA IDA +COAST NON ABUTS ABI MAN HUB ont won mon +COAST WON ABUTS ONT LSU MIN non man +COAST ONT ABUTS WON MON NYS LON LER MIC UPE LHU LSU BEA non +COAST ABI ABUTS HUB UNG NON que mon +COAST UNG ABUTS LAB ABI HUB SOL NUN cot que +COAST LAB ABUTS GSL COT UNG SOL +COAST COT ABUTS LAB GSL QUE ung +COAST QUE ABUTS COT GSL GAS BEA MON ung abi +COAST MON ABUTS QUE BEA ONT abi non +COAST BEA ABUTS QUE NYS MON ONT GAS mai vem +COAST GAS ABUTS NBR/NC QUE GSL BEA mai +COAST NBR/NC ABUTS GAS GSL NSC +COAST NBR/SC ABUTS NSC MAB MAI +COAST nbr ABUTS NSC MAI GAS GSL MAB +COAST NSC ABUTS GSL NAO MAB NBR/SC NBR/NC +COAST NEW ABUTS NAO GSL SOL +COAST WAS ABUTS VAN ORE SJF IDA +COAST ORE ABUTS WAS SFR WSB SJF IDA NEV +LAND IDA ABUTS VAN MTA WYO UTA NEV ORE WAS +COAST SFR ABUTS ORE LAN WSB NEV +COAST LAN ABUTS SFR SDI GSC WSB NEV +COAST SDI ABUTS LAN BAJ GSC NEV ARI +LAND ARI ABUTS NEV UTA COL NME CHH BAJ SDI +LAND UTA ABUTS IDA WYO COL ARI NEV +LAND NEV ABUTS IDA UTA ARI SDI LAN SFR ORE +LAND NME ABUTS COL OKL DAL WTE CHH ARI +LAND COL ABUTS NEB KAN OKL NME ARI UTA WYO +LAND WYO ABUTS MTA DAK NEB COL UTA IDA +LAND MTA ABUTS CGY SAS DAK WYO IDA VAN +LAND DAL ABUTS OKL ARK LOU HOU SAN WTE NME +LAND OKL ABUTS KAN MIS ARK DAL NME COL +LAND KAN ABUTS MIS OKL COL NEB +LAND NEB ABUTS IOW MIS KAN COL WYO DAK +LAND DAK ABUTS SAS MAN MIN IOW NEB WYO MTA +COAST MIN ABUTS WON LSU MIL/NC IOW DAK man +LAND IOW ABUTS MIN MIL CHI MIS NEB DAK +COAST MIS ABUTS CHI KEN TEN ARK IOW OKL KAN NEB +COAST ARK ABUTS MIS TEN DSO LOU DAL OKL +COAST LOU ABUTS ARK DSO APB GOM HOU DAL +COAST HOU ABUTS LOU GOM SAN DAL +COAST SAN ABUTS HOU GOM NLE WTE DAL COA +LAND WTE ABUTS DAL SAN COA CHH NME +WATER LON ABUTS NYS ONT +COAST UPE ABUTS ONT LHU MIC LMI MIL/EC MIL/NC LSU +COAST MIC ABUTS UPE LHU ONT LER OHI IND LMI +COAST IND ABUTS LMI MIC CHI ohi ken +COAST CHI ABUTS MIL/EC LMI IND KEN MIS IOW +WATER LMI ABUTS UPE MIC IND CHI MIL/EC LHU +WATER LSU ABUTS ONT UPE MIL/NC MIN WON +WATER LHU ABUTS ONT MIC UPE LMI +WATER LER ABUTS NYS WPE OHI MIC ONT +COAST MIL/NC ABUTS MIN LSU UPE +COAST MIL/EC ABUTS UPE LMI CHI +COAST mil ABUTS UPE CHI IOW MIN LSU LMI +COAST OHI ABUTS LER WPE MIC WVI ken ind +COAST WPE ABUTS NYS OHI LER PHI wdc WVI +COAST NYS ABUTS BEA NYC WPE LER ONT LON vem mas PHI +COAST MAI ABUTS NBR/SC MAB VEM gas bea +COAST VEM ABUTS MAI MAB MAS nys bea +COAST MAS ABUTS VEM MAB CMA NYC nys +COAST NYC ABUTS NYS MAS CMA NJE PHI +COAST NJE ABUTS NYC CMA WDC PHI +COAST WDC ABUTS NJE CMA CHB VIR wpe PHI WVI +LAND PHI ABUTS NYS NYC NJE WDC WPE +COAST VIR ABUTS WDC CHB NCA WVI ten ken +LAND WVI ABUTS WPE WDC VIR KEN OHI +COAST KEN ABUTS CHI TEN MIS ind ohi WVI vir +COAST TEN ABUTS KEN DSO ARK MIS vir nca geo +COAST DSO ABUTS TEN FPA APB LOU ARK geo +COAST GEO ABUTS SCA ESB JAC ten nca fpa dso +COAST SCA ABUTS ESB GEO NCA +COAST NCA ABUTS VIR CHB ESB SCA geo ten +COAST FPA ABUTS TAM APB DSO geo jac +COAST JAC ABUTS GEO ESB MIA tam fpa +COAST MIA ABUTS TAM JAC ESB SOF APB +COAST TAM ABUTS MIA APB FPA jac +COAST HAV ABUTS CAM/NC CAM/SC WCS SOY SOF +COAST CAM/NC ABUTS HAV SOF BET HOL +COAST CAM/SC ABUTS HOL CAT WCS HAV +COAST cam ABUTS HOL HAV CAT WCS SOF BET +COAST HOL ABUTS CAT CAM/SC CAM/NC BET +COAST HAI ABUTS DOM ECS CAT BET +COAST DOM ABUTS SOS LES ECS HAI BET +COAST KIN ABUTS ECS WCS CAT +COAST BAJ ABUTS GSC SDI CHH GCA NPO MPO ARI +COAST CHH ABUTS BAJ GCA DUR WTE COA ARI NME +LAND COA ABUTS WTE SAN NLE DUR CHH POT +COAST NLE ABUTS SAN GOM GOC POT COA +COAST DUR ABUTS CHH GUA GCA COA pot +COAST GUA ABUTS GUE GCA COM DUR pot MEX +COAST POT ABUTS NLE GOC VER MEX gua dur COA +COAST VER ABUTS POT GOC TAB chp oax MEX +LAND MEX ABUTS POT VER OAX GUE GUA +COAST GUE ABUTS GUA OAX COM MEX +COAST OAX ABUTS GUE CHP GOT COM MEX ver +COAST CHP ABUTS GUT/SC GOT OAX ver tab +COAST TAB ABUTS GOC YUC VER gut chp +COAST GUT/SC ABUTS ESA GOT COM CHP +COAST GUT/EC ABUTS YUC GOH HON/NC +COAST gut ABUTS YUC HON ESA CHP TAB GOT COM GOH +COAST YUC ABUTS GOM SOY GOH GUT/EC TAB GOC +COAST ESA ABUTS HON/SC COM GOF GUT/SC +COAST HON/NC ABUTS GUT/EC GOH WCS NIC/EC +COAST HON/SC ABUTS NIC/WC GOF ESA +COAST hon ABUTS NIC ESA GUT GOH WCS GOF +COAST NIC/EC ABUTS HON/NC WCS GMO COS/NC +COAST NIC/WC ABUTS COS/SC COB GOF HON/SC +COAST nic ABUTS COS HON WCS GMO COB GOF +COAST COS/NC ABUTS PAN GMO NIC/EC +COAST COS/SC ABUTS PAN COB NIC/WC +COAST cos ABUTS PAN NIC GMO COB +COAST PAN ABUTS GMO SCS CAL/NC CAL/SC GOP COB COS/SC COS/NC +COAST CAL/NC ABUTS PAN SCS ANT +COAST CAL/SC ABUTS ECU GOG GOP PAN +COAST cal ABUTS ANT BOG ECU PAN SCS GOG GOP +COAST ANT ABUTS GUJ CAL/NC SCS VIC BOG +COAST GUJ ABUTS VEN ANT SCS VIC +COAST VEN ABUTS GUJ SCS ECS LES VIC +LAND VIC ABUTS VEN BOG ANT GUJ +LAND BOG ABUTS ANT VIC LIM ECU CAL +COAST LIM ABUTS GAL GOG ECU BOG +COAST ECU ABUTS LIM GOG CAL/SC BOG +COAST HAW ABUTS SPO SWP MPO NPO +COAST GRE ABUTS SOL BAB ARO NAO +WATER BER ABUTS ANC GOA ARO +WATER ARO ABUTS BAB NUN NWT BEF ANC BER GRE +WATER BEF ABUTS NWT YUK ANC ARO NUN +WATER HUB ABUTS SOL UNG ABI NON MAN NUN +WATER BAB ABUTS GRE SOL NUN ARO +WATER SOL ABUTS GRE NAO NEW GSL LAB UNG HUB NUN BAB +WATER NAO ABUTS SOL GRE MAO MAB NSC GSL NEW +WATER GSL ABUTS LAB SOL NEW NAO NSC NBR/NC GAS QUE COT +WATER MAB ABUTS MAI NBR/SC NSC NAO MAO CMA MAS VEM +WATER MAO ABUTS NAO SOS BET CMA MAB +WATER CHB ABUTS WDC CMA ESB NCA VIR +WATER CMA ABUTS NYC MAS MAB MAO BET ESB CHB WDC NJE +WATER SOS ABUTS MAO LES DOM BET +WATER BET ABUTS MAO SOS DOM HAI CAT HOL CAM/NC SOF ESB CMA +WATER ESB ABUTS SCA NCA CHB CMA BET SOF MIA JAC GEO +WATER SOF ABUTS APB MIA ESB BET CAM/NC HAV SOY GOM +WATER ECS ABUTS HAI DOM LES VEN SCS WCS KIN CAT +WATER LES ABUTS VEN ECS DOM SOS +WATER APB ABUTS DSO FPA TAM MIA SOF GOM LOU +WATER GOM ABUTS SAN HOU LOU APB SOF SOY YUC GOC NLE +WATER GOC ABUTS NLE GOM YUC TAB VER POT +WATER SOY ABUTS GOM SOF HAV WCS GOH YUC +WATER GOH ABUTS SOY WCS HON/NC GUT/EC YUC +WATER CAT ABUTS CAM/SC HOL BET HAI ECS KIN WCS +WATER WCS ABUTS GOH SOY HAV CAM/SC CAT KIN ECS SCS GMO NIC/EC HON/NC +WATER SCS ABUTS ECS VEN GUJ ANT CAL/NC PAN GMO WCS +WATER GMO ABUTS WCS SCS PAN COS/NC NIC/EC +WATER GOA ABUTS ANC QCS SJF WSB NPO BER +WATER QCS ABUTS ANC VAN SJF GOA +WATER SJF ABUTS QCS VAN WAS ORE WSB GOA +WATER WSB ABUTS SJF ORE SFR LAN GSC NPO GOA +WATER NPO ABUTS WSB GSC BAJ MPO HAW SWP GOA +WATER MPO ABUTS BAJ GCA COM SPO HAW NPO +WATER GSC ABUTS LAN SDI BAJ NPO WSB +WATER SWP ABUTS HAW SPO NPO +WATER COM ABUTS GCA GUE OAX GOT GUT/SC ESA GOF GAL SPO MPO GUA +WATER GCA ABUTS BAJ CHH DUR GUA COM MPO +WATER GOT ABUTS OAX CHP GUT/SC COM +WATER SPO ABUTS HAW MPO COM GAL SWP +WATER GAL ABUTS GOF GOG LIM SPO COM +WATER GOG ABUTS COB GOP CAL/SC ECU LIM GAL GOF +WATER GOP ABUTS PAN CAL/SC GOG COB +WATER COB ABUTS NIC/WC COS/SC PAN GOP GOG GOF +WATER GOF ABUTS ESA HON/SC NIC/WC COB GOG GAL COM diff --git a/diplomacy/maps/known_world_901.map b/diplomacy/maps/known_world_901.map new file mode 100644 index 0000000..e6f1914 --- /dev/null +++ b/diplomacy/maps/known_world_901.map @@ -0,0 +1,710 @@ +BEGIN SPRING 901 MOVEMENT +RULE BUILD_ANY + +ARABIA (ARABIAN:A) ARD BAG BSR ISF +A BAG +A ARD +A ISF +F BSR + +BYZANTINUM (BYZANTINUM:B) ATT CHS CON TAR +F TAR +A CON +F CHS +F ATT + +CHINA (CHINA:C) CHA GUA NAN YAN +A CHA +A YAN +A NAN +F GUA + +DENMARK (DENMARK:D) JLN JOR SCA VIK +F JOR/EC +F JLN +F SCA +A VIK + +EGYPT (EGYPTIAN:E) ALE AQA BAR JER +F BAR +A ALE +A AQA +F JER + +FRANCE (FRENCH:F) AQT GAS NAR PAR +F PAR +A AQT +A GAS +A NAR + +GERMANY (GERMAN:G) BAV BRE SAX SWA +F BRE +A SWA +A SAX +A BAV + +INDIA (INDIAN:I) IND KNJ UJJ VAR +F UJJ +A IND +A KNJ +A VAR + +KHAZARIA (KHAZARIAN:K) ATI BAL SRK TAM +A TAM +A SRK +A ATI +A BAL + +RUSSIA (RUSSIAN:R) KIE NOV ROS SMO +A SMO +F NOV +A ROS +A KIE + +SPAIN (SPANISH:S) CAD COR SAL VAL +F CAD +A SAL +A COR +F VAL + +TURAN (TURAN:T) BUK HER SAM URG +A URG +A BUK +A SAM +A HER + +SRIVIJAYA (SRIVIJAYAN:V) CAH JAM KAL PLM +F KAL +F CAH +F PLM +A JAM + +WAGADU (WAGADUN:W) AWL KUS NIO WAL +F AWL +A KUS +A WAL +A NIO + +AXUM (AXUM:X) ADU AXU MAL ROH +A AXU +A ROH +F ADU +A MAL + +NEUTRAL-STATE (NEUTRAL:N) ARM AZE BAS BJA BOR BRI BUL BUT CHO CRE CRS CYP DAL DUB EST GEO GHU IFR JEJ KAM KAN KAS LBU LOT MAH MAK MAN MAU MAZ MOR ORB PAG PAM PEC ROM SAI SAR SER SIC SIL SOC THR TIB TKA UYG WSX YEM ZAW ZIM +DUMMY +A ARM +A AZE +A BAS +A BJA +A BOR +A BRI +A BUL +A BUT +A CHO +A CRE +A CRS +A CYP +A DAL +A DUB +A EST +A GEO +A GHU +A IFR +A JEJ +A KAM +A KAN +A KAS +A LBU +A LOT +A MAH +A MAK +A MAN +A MAU +A MAZ +A MOR +A ORB +A PAG +A PAM +A PEC +A ROM +A SAI +A SAR +A SER +A SIC +A SIL +A SOC +A THR +A TIB +A TKA +A UYG +A WSX +A YEM +A ZAW +A ZIM + + +Abkhazia = ABK +Abodrite Sea = ABS +Adulis = ADU +Aegean Sea = AES +Alexandria = ALE +Alidistan = ALI +Amdo = AMD +Annam = ANN +Andaman Sea = ANS +Al-Qatta'i = AQA +Aquitaine = AQT +Aquileia = AQU +Ardebil = ARD +Armenia = ARM +Arabian Sea = ARS +Assode = ASS +Asturias = AST +Atil = ATI +Antipodean Sea = ATS +Attalia = ATT +Autun = AUT +Awdagost = AWD +Awlil = AWL +Province of Axum = AXU +Azania = AZA +Azerbaijan = AZE +Baghdad = BAG +Balanjar = BAL +Barca = BAR +Bashkortostan = BAS +Bavaria = BAV +Berenice = BER +Bjarmaland = BJA +Balhae = BLH +Balhae (East Coast) = BLH/EC +Balhae (South Coast) = BLH/SC +Balkh = BLK +Balearic Sea = BLS +Borussia = BOR +Brittish Channel = BRC +Bremen = BRE +Barghawata = BRG +Brittany = BRI +Bassikinu = BSK +Basra = BSR +Buccellaria = BUC +Bukhara = BUK +Bulgar = BUL +Bure = BUR +Butuan = BUT +Cadiz = CAD +Cahaya = CAH +Cappadocia = CAP +Cantabric Sea = CAS +Chang'an = CHA +Cheremissia = CHE +Chalukya = CHL +Champa = CHM +Changcheng = CHN +Chola = CHO +Cherson = CHS +Coromandel Bay = CIB +Cilician Sea = CIS +Caledonia = CLD +Constantinople = CON +Cordova = COR +Crete = CRE +Corsica = CRS +Cyprus = CYP +Daju = DAJ +Dalmatia = DAL +Damascus = DAM +Derbent = DER +Dregovichia = DRE +Dublin = DUB +Dvaravati = DVA +Eastern Sea = EAS +East Euxine Sea = EES +Egyptian Sea = EGS +Epirus = EPI +Esteland = EST +Fazzan = FAZ +Finnish Sea = FIS +Franconia = FRA +Friesland = FRI +Gulf of Aden = GAD +Galicia = GAL +Gascony = GAS +Georgia = GEO +Ghuzz = GHU +Gokomere Sea = GOS +Gottland = GOT +Granada = GRA +Guangzhou = GUA +Helvetia = HEL +Henan = HEN +Herat = HER +Icelandic Sea = ICS +Ifriqiya = IFR +Illyrian Sea = ILS +Indraprastha = IND +Ionian Sea = IOS +Isfahan = ISF +Jambi = JAM +Java Sea = JAS +Jenne-Jeno = JEJ +Jerusalem = JER +Jiangxi = JIA +Jelling = JLN +Jorvik = JOR +Jorvik (East Coast) = JOR/EC +Jorvik (West Coast) = JOR/WC +Kakheti = KAK +Kalimantan = KAL +Kambuja = KAM +Kanem = KAN +Karelia = KAR +Kashmir = KAS +Kattegat = KAT +Khitai = KHI +Khorasan = KHO +Kiev = KIE +Kipchak = KIP +Kangaba = KNG +Kannauj = KNJ +Kongo = KON +Kornia = KOR +Kotoko = KOT +Krivichia = KRI +Karimata Straits = KRS +Kumbi Saleh = KUS +Kutamia = KUT +Kyzyl Kum = KYK +Kyrgyzstan = KYR +Lower Burgundy = LBU +Lettish Sea = LES +Ligurian Sea = LGS +Libyan Sea = LIS +Livonia = LIV +Lombardy = LOM +Lothairingia = LOT +Luba = LUB +Luo = LUO +Lusong Sea = LUS +Macedonia = MAC +Mahilaka = MAH +Makuran = MAK +Malao = MAL +Mansurah = MAN +Malabar Sea = MAS +Mauretania = MAU +Mazovia = MAZ +Mecca = MEC +Merkit = MER +Menuthian Sea = MES +Minang Sea = MIS +Moravia = MOR +Mosul = MOS +Mordvinia = MRD +Meroe = MRO +Munster = MUN +Namib = NAM +Nanjing = NAN +Narbonne = NAR +Nefud = NEF +Nepal = NEP +North German Sea = NGS +Niore = NIO +North Khazar Sea = NKS +Nanzhao = NNZ +Norway = NOR +North Ocean Sea = NOS +Novgorod = NOV +North Red Sea = NRS +Oman = OMA +Onoguria = ONO +Ordu-Balyk = ORB +Pagan = PAG +Pala = PAL +Pamplona = PAM +Paris = PAR +Pechenega = PEC +Pechenega (East Coast) = PEC/EC +Pechenega (West Coast) = PEC/WC +Phebol = PHE +Palembang = PLM +Polania = POL +Pomerania = POM +Qarluk = QAR +Rastrakuta = RAS +Rhapta = RHA +Roha = ROH +Rome = ROM +Rome (East Coast) = ROM/EC +Rome (West Coast) = ROM/WC +Rostov = ROS +Saamiland = SAA +Saamiland (North Coast) = SAA/NC +Saamiland (South Coast) = SAA/SC +Sabah = SAB +Saikaido = SAI +Salamanca = SAL +Samarkand = SAM +Sanhaja = SAN +Sardinia = SAR +Saxony = SAX +Scania = SCA +Sea of Cham = SCH +Serendib = SER +Severyana = SEV +South German Sea = SGS +Shahiya = SHA +Sicily = SIC +Sichuan = SIH +Sijilmassa = SIJ +Silla = SIL +Sijistan = SJS +Straits of Jebel Tarik = SJT +South Khazar Sea = SKS +Slavonia = SLA +Salerno = SLR +Smolensk = SMO +Sea of Nihon = SNI +Socotra = SOC +Sogdia = SOG +South Ocean Sea = SOS +Spanish March = SPM +Spoleto = SPO +Sarkel = SRK +South Red Sea = SRS +Sea of Tangiers = STA +Stone Belt = STB +Sea of Tepanga = STP +Sea of Tyre = STY +Sudd = SUD +Southern Sea = SUS +Swabia = SWA +Sea of Worms = SWO +Tahert = TAH +Tamantarka = TAM +Taranto = TAR +Takrur Sea = TAS +Thrace = THR +Tibet = TIB +Tiraqqa = TIR +Tkanaren = TKA +Toledo = TOL +Toulouse = TOU +Tripolitania = TRI +Tunguz = TUN +Tyrrhenian Sea = TYS +Upper Burgundy = UBU +Udmurtia = UDM +Ujjain = UJJ +Uppland = UPP +Urgench = URG +Uyghurstan = UYG +Valencia = VAL +Varanasi = VAR +Veletia = VEL +Veletia (North Coast) = VEL/NC +Veletia (West Coast) = VEL/WC +Viken = VIK +Vlacha = VLA +Volhynia = VOL +Vyatichia = VYA +Walata = WAL +West Euxine Sea = WES +White Sea = WHS +Wales = WLS +Welsh Sea = WSS +Wessex = WSX +Western Sea = WTS +Yanjing = YAN +Yemen = YEM +Yellow Sea = YES +Yugra = YUG +Zaragoza = ZAR +Zawila = ZAW +Zeila = ZEI +Zimbabwe = ZIM + +COAST ABK ABUTS bal EES GEO TAM +WATER ABS ABUTS BOR JLN KAT LES POM SCA VEL/NC +COAST ADU ABUTS AXU GAD MRO ROH SRS ZEI +WATER AES ABUTS ATT CIS CON CRE IOS +COAST ALE ABUTS AQA BAR EGS FAZ JER MEC NRS tri +COAST ALI ABUTS ARD ISF KHO SKS URG +LAND AMD ABUTS CHA SOG TIB UYG +COAST ANN ABUTS CHM dva GUA kam NNZ SCH +WATER ANS ABUTS CAH CIB DVA JAM KRS MIS SER +COAST AQA ABUTS ALE BER DAJ FAZ MAK NRS +COAST AQT ABUTS AUT BRI CAS GAS par TOU +COAST AQU ABUTS BAV DAL ILS lom ROM/EC SLA +COAST ARD ABUTS ALI AZE BAG ISF MOS SKS +LAND ARM ABUTS CAP DAM KAK MOS +WATER ARS ABUTS BSR GAD MAN MAS OMA SOC UJJ YEM +LAND ASS ABUTS DAJ FAZ KAN KOT SIJ TAH TIR WAL ZAW +COAST AST ABUTS CAS GAL PAM SAL ZAR +COAST ATI ABUTS BAL BAS KIP MRD NKS SRK UDM +WATER ATS ABUTS GOS MAH MES PHE SOS SUS WTS +COAST ATT ABUTS AES buc CAP CIS CON +LAND AUT ABUTS AQT LBU LOT NAR PAR TOU UBU +LAND AWD ABUTS AWL BUR KUS NIO SAN +COAST AWL ABUTS AWD BUR SAN SOS TAS +LAND AXU ABUTS ADU MRO ROH SUD +COAST AZA ABUTS GAD MAL MES RHA +COAST AZE ABUTS ARD DER KAK MOS SKS +LAND BAG ABUTS ARD BSR DAM ISF MOS NEF +COAST BAL ABUTS abk ATI DER geo KAK NKS SRK tam +COAST BAR ABUTS ALE EGS LIS TRI +LAND BAS ABUTS ATI KIP STB UDM YUG +LAND BAV ABUTS AQU HEL LOM MOR POL SAX SLA SWA +COAST BER ABUTS AQA MAK MRO NRS SRS +COAST BJA ABUTS che ICS kar KOR SAA/NC WHS +COAST blh ABUTS KHI MER SIL SNI YAN YES +COAST BLH/EC ABUTS SIL SNI +COAST BLH/SC ABUTS SIL YAN YES +LAND BLK ABUTS BUK HER KAS MAN SAM SHA SOG +WATER BLS ABUTS CRS LGS SAR SJT SPM TYS VAL +COAST BOR ABUTS ABS DRE LES LIV MAZ POL POM +WATER BRC ABUTS BRI LOT NOS PAR SGS WSX +COAST BRE ABUTS FRA FRI SAX SGS VEL/WC +COAST BRG ABUTS MAU SAN SIJ STA TAS +COAST BRI ABUTS AQT BRC CAS NOS PAR +LAND BSK ABUTS JEJ KOT NIO TIR WAL +COAST BSR ABUTS ARS BAG ISF MAN NEF OMA SJS +COAST BUC ABUTS att cap CON EES GEO WES +LAND BUK ABUTS BLK HER KYK SAM URG +LAND BUL ABUTS CHE KOR MRD UDM VYA +COAST BUR ABUTS AWD AWL KNG NIO SOS +COAST BUT ABUTS JAS LUS SCH STP +COAST CAD ABUTS COR GAL GRA NOS SAL SJT STA +COAST CAH ABUTS ANS DVA KAM KRS +COAST CAP ABUTS ARM ATT buc CIS DAM geo KAK STY +WATER CAS ABUTS AQT AST BRI GAL GAS NOS PAM +LAND CHA ABUTS AMD CHN HEN JIA NNZ SIH TIB UYG YAN +COAST CHE ABUTS bja BUL kar kor KRI ROS VYA +COAST CHL ABUTS CHO CIB KNJ ras VAR +COAST CHM ABUTS ANN KAM SCH +LAND CHN ABUTS CHA KHI ORB UYG YAN +COAST CHO ABUTS CHL CIB MAS RAS +COAST CHS ABUTS EES PEC/EC PEC/WC WES +WATER CIB ABUTS ANS CHL CHO DVA MAS PAL SER VAR +WATER CIS ABUTS AES ATT CAP CRE CYP EGS STY +COAST CLD ABUTS ICS JOR/EC JOR/WC WSS +COAST CON ABUTS AES ATT BUC EPI IOS MAC THR WES +LAND COR ABUTS CAD GRA SAL TOL VAL +COAST CRE ABUTS AES CIS EGS IOS +COAST CRS ABUTS BLS LGS TYS +COAST CYP ABUTS CIS EGS STY +LAND DAJ ABUTS AQA ASS FAZ KAN MAK SUD ZAW +COAST DAL ABUTS AQU EPI ILS MAC ONO SLA +COAST DAM ABUTS ARM BAG CAP JER mec MOS NEF STY +COAST DER ABUTS AZE BAL KAK NKS SKS +LAND DRE ABUTS BOR KIE LIV MAZ SMO VOL +COAST DUB ABUTS ICS MUN NOS WSS +COAST DVA ABUTS ann ANS CAH CIB kam NNZ PAG PAL +WATER EAS ABUTS GUA LUS NAN SAI SIL SNI STP YES +WATER EES ABUTS ABK BUC CHS GEO PEC/EC TAM WES +WATER EGS ABUTS ALE BAR CIS CRE CYP IOS JER LIS STY +COAST EPI ABUTS CON DAL ILS IOS MAC +COAST EST ABUTS FIS LIV NOV +LAND FAZ ABUTS ALE AQA ASS DAJ IFR TAH TRI ZAW +WATER FIS ABUTS EST KAR LES LIV NOV SAA/SC UPP +LAND FRA ABUTS BRE FRI LOT SAX SWA +COAST FRI ABUTS BRE FRA LOT SGS +WATER GAD ABUTS ADU ARS AZA MAH MAL MAS MES PHE SOC SRS YEM ZEI +COAST GAL ABUTS AST CAD CAS NOS SAL +COAST GAS ABUTS AQT CAS PAM TOU +COAST GEO ABUTS ABK bal BUC cap EES KAK +COAST GHU ABUTS KIP KYK NKS URG +WATER GOS ABUTS ATS MES NAM SOS ZIM +COAST GOT ABUTS LES SCA UPP vik +COAST GRA ABUTS CAD COR SJT VAL +COAST GUA ABUTS ANN EAS JIA NAN NNZ SCH STP +LAND HEL ABUTS BAV LBU LOM SWA UBU +COAST HEN ABUTS CHA JIA NAN YAN YES +LAND HER ABUTS BLK BUK KHO MAN SJS URG +WATER ICS ABUTS BJA CLD DUB JOR/EC MUN NGS NOR NOS SAA/NC SWO WHS WSS +COAST IFR ABUTS FAZ KUT LIS SJT TAH TRI TYS +WATER ILS ABUTS AQU DAL EPI IOS ROM/EC SPO TAR +LAND IND ABUTS KAS KNJ NEP SHA UJJ +WATER IOS ABUTS AES CON CRE EGS EPI ILS LIS SIC TAR TYS +LAND ISF ABUTS ALI ARD BAG BSR KHO SJS +COAST JAM ABUTS ANS KRS MIS PLM SUS +WATER JAS ABUTS BUT KAL KRS LUS PLM SAB SCH SUS +LAND JEJ ABUTS BSK KOT TIR +COAST JER ABUTS ALE DAM EGS mec STY +LAND JIA ABUTS CHA GUA HEN NAN NNZ +COAST JLN ABUTS ABS KAT NGS SGS VEL/NC VEL/WC +COAST jor ABUTS CLD ICS NGS WLS WSS WSX +COAST JOR/EC ABUTS CLD ICS NGS WSX +COAST JOR/WC ABUTS CLD WLS WSS +LAND KAK ABUTS ARM AZE BAL CAP DER GEO MOS +COAST KAL ABUTS JAS KRS SAB SCH +COAST KAM ABUTS ann CAH CHM dva KRS SCH +LAND KAN ABUTS ASS DAJ KOT LUO SUD +COAST KAR ABUTS bja che FIS NOV ROS SAA/SC +LAND KAS ABUTS BLK IND NEP SHA SOG TIB +WATER KAT ABUTS ABS JLN NGS NOR SCA VIK +LAND KHI ABUTS BLH CHN MER ORB YAN +LAND KHO ABUTS ALI HER ISF SJS URG +LAND KIE ABUTS DRE PEC SEV SMO VLA VOL VYA +COAST KIP ABUTS ATI BAS GHU KYK KYR NKS yug +COAST KNG ABUTS BUR KOT NIO SOS +LAND KNJ ABUTS CHL IND NEP RAS UJJ VAR +COAST KON ABUTS KOT LUB NAM SOS zim +COAST KOR ABUTS BJA BUL che STB UDM WHS YUG +COAST KOT ABUTS ASS BSK JEJ KAN KNG KON LUB LUO NIO SOS TIR +LAND KRI ABUTS CHE NOV ROS SMO VYA +WATER KRS ABUTS ANS CAH JAM JAS KAL KAM PLM SCH +LAND KUS ABUTS AWD NIO SAN WAL +COAST KUT ABUTS IFR MAU SJT TAH +LAND KYK ABUTS BUK GHU KIP KYR QAR SAM URG +LAND KYR ABUTS KIP KYK ORB QAR TUN YUG +COAST LBU ABUTS AUT HEL LGS LOM NAR UBU +WATER LES ABUTS ABS BOR FIS GOT LIV SCA UPP +WATER LGS ABUTS BLS CRS LBU LOM NAR ROM/WC SPM TYS +WATER LIS ABUTS BAR EGS IFR IOS SIC TRI TYS +COAST LIV ABUTS BOR DRE EST FIS LES nov SMO +COAST LOM ABUTS aqu BAV HEL LBU LGS ROM/WC +COAST LOT ABUTS AUT BRC FRA FRI PAR SGS SWA UBU +LAND LUB ABUTS KON KOT LUO MAL RHA ROH ZIM +LAND LUO ABUTS KAN KOT LUB ROH SUD +WATER LUS ABUTS BUT EAS JAS SAI SNI STP +LAND MAC ABUTS CON DAL EPI ONO THR +COAST MAH ABUTS ATS GAD MAS PHE SUS +LAND MAK ABUTS AQA BER DAJ MRO SUD +COAST MAL ABUTS AZA GAD LUB rha ROH ZEI +COAST MAN ABUTS ARS BLK BSR HER SHA SJS UJJ +WATER MAS ABUTS ARS CHO CIB GAD MAH MIS RAS SER SOC SUS UJJ +COAST MAU ABUTS BRG KUT SIJ SJT STA TAH +LAND MAZ ABUTS BOR DRE POL VOL +COAST MEC ABUTS ALE dam jer NEF NRS SRS YEM +LAND MER ABUTS BLH KHI ORB TUN +WATER MES ABUTS ATS AZA GAD GOS PHE RHA ZIM +WATER MIS ABUTS ANS JAM MAS SER SUS +LAND MOR ABUTS BAV ONO POL SLA VOL +LAND MOS ABUTS ARD ARM AZE BAG DAM KAK +LAND MRD ABUTS ATI BUL SEV SRK UDM VYA +COAST MRO ABUTS ADU AXU BER MAK SRS SUD +COAST MUN ABUTS DUB ICS NOS +COAST NAM ABUTS GOS KON SOS ZIM +COAST NAN ABUTS EAS GUA HEN JIA YES +COAST NAR ABUTS AUT LBU LGS SPM TOU +LAND NEF ABUTS BAG BSR DAM MEC OMA YEM +LAND NEP ABUTS IND KAS KNJ PAL TIB VAR +WATER NGS ABUTS ICS JLN JOR/EC KAT NOR SGS WSX +LAND NIO ABUTS AWD BSK BUR KNG KOT KUS WAL +WATER NKS ABUTS ATI BAL DER GHU KIP SKS URG +LAND NNZ ABUTS ANN CHA DVA GUA JIA PAG PAL SIH TIB +COAST NOR ABUTS ICS KAT NGS SAA/NC upp VIK +WATER NOS ABUTS BRC BRI CAD CAS DUB GAL ICS MUN STA SWO WSS WSX +COAST NOV ABUTS EST FIS KAR KRI liv ROS SMO +WATER NRS ABUTS ALE AQA BER MEC SRS +COAST OMA ABUTS ARS BSR NEF YEM +LAND ONO ABUTS DAL MAC MOR SLA THR VLA VOL +LAND ORB ABUTS CHN KHI KYR MER QAR SAM TUN UYG +LAND PAG ABUTS DVA NNZ PAL +COAST PAL ABUTS CIB DVA NEP NNZ PAG TIB VAR +COAST PAM ABUTS AST CAS GAS spm TOU ZAR +COAST PAR ABUTS aqt AUT BRC BRI LOT +COAST pec ABUTS CHS EES KIE SEV TAM VLA WES +COAST PEC/EC ABUTS CHS EES TAM +COAST PEC/WC ABUTS CHS VLA WES +COAST PHE ABUTS ATS GAD MAH MES +COAST PLM ABUTS JAM JAS KRS SUS +LAND POL ABUTS BAV BOR MAZ MOR POM SAX VOL +COAST POM ABUTS ABS BOR POL SAX VEL/NC +LAND QAR ABUTS KYK KYR ORB SAM +COAST RAS ABUTS chl CHO KNJ MAS UJJ +COAST RHA ABUTS AZA LUB mal MES ZIM +LAND ROH ABUTS ADU AXU LUB LUO MAL SUD ZEI +COAST rom ABUTS AQU ILS LGS LOM SLR SPO TYS +COAST ROM/EC ABUTS AQU ILS SPO +COAST ROM/WC ABUTS LGS LOM SLR TYS +LAND ROS ABUTS CHE KAR KRI NOV +COAST saa ABUTS BJA FIS ICS KAR NOR UPP +COAST SAA/NC ABUTS BJA ICS NOR +COAST SAA/SC ABUTS FIS KAR UPP +COAST SAB ABUTS JAS KAL SCH +COAST SAI ABUTS EAS LUS SNI +LAND SAL ABUTS AST CAD COR GAL TOL ZAR +LAND SAM ABUTS BLK BUK KYK ORB QAR SOG UYG +COAST SAN ABUTS AWD AWL BRG KUS SIJ TAS WAL +COAST SAR ABUTS BLS SJT TYS +LAND SAX ABUTS BAV BRE FRA POL POM SWA VEL +COAST SCA ABUTS ABS GOT KAT LES VIK +WATER SCH ABUTS ANN BUT CHM GUA JAS KAL KAM KRS SAB STP +COAST SER ABUTS ANS CIB MAS MIS +LAND SEV ABUTS KIE MRD PEC SRK TAM VYA +WATER SGS ABUTS BRC BRE FRI JLN LOT NGS VEL/WC WSX +LAND SHA ABUTS BLK IND KAS MAN UJJ +COAST SIC ABUTS IOS LIS TYS +LAND SIH ABUTS CHA NNZ TIB +LAND SIJ ABUTS ASS BRG MAU SAN TAH WAL +COAST SIL ABUTS BLH/EC BLH/SC EAS SNI YES +LAND SJS ABUTS BSR HER ISF KHO MAN +WATER SJT ABUTS BLS CAD GRA IFR KUT MAU SAR STA TYS VAL +WATER SKS ABUTS ALI ARD AZE DER NKS URG +LAND SLA ABUTS AQU BAV DAL MOR ONO +COAST SLR ABUTS ROM/WC spo TAR TYS +LAND SMO ABUTS DRE KIE KRI LIV NOV VYA +WATER SNI ABUTS BLH/EC EAS LUS SAI SIL +COAST SOC ABUTS ARS GAD MAS +LAND SOG ABUTS AMD BLK KAS SAM TIB UYG +WATER SOS ABUTS ATS AWL BUR GOS KNG KON KOT NAM TAS WTS +COAST SPM ABUTS BLS LGS NAR pam TOU VAL ZAR +COAST SPO ABUTS ILS ROM/EC slr TAR +LAND SRK ABUTS ATI BAL MRD SEV TAM +WATER SRS ABUTS ADU BER GAD MEC MRO NRS YEM +WATER STA ABUTS BRG CAD MAU NOS SJT SWO TAS TKA +LAND STB ABUTS BAS KOR UDM YUG +WATER STP ABUTS BUT EAS GUA LUS SCH +WATER STY ABUTS CAP CIS CYP DAM EGS JER +LAND SUD ABUTS AXU DAJ KAN LUO MAK MRO ROH +WATER SUS ABUTS ATS JAM JAS MAH MAS MIS PLM +LAND SWA ABUTS BAV FRA HEL LOT SAX UBU +WATER SWO ABUTS ICS NOS STA TKA WTS +LAND TAH ABUTS ASS FAZ IFR KUT MAU SIJ +COAST TAM ABUTS ABK bal EES PEC/EC SEV SRK +COAST TAR ABUTS ILS IOS SLR SPO TYS +WATER TAS ABUTS AWL BRG SAN SOS STA TKA WTS +COAST THR ABUTS CON MAC ONO VLA WES +LAND TIB ABUTS AMD CHA KAS NEP NNZ PAL SIH SOG +LAND TIR ABUTS ASS BSK JEJ KOT WAL +COAST TKA ABUTS STA SWO TAS WTS +LAND TOL ABUTS COR SAL VAL ZAR +LAND TOU ABUTS AQT AUT GAS NAR PAM SPM +COAST TRI ABUTS ale BAR FAZ IFR LIS +COAST TUN ABUTS KYR MER ORB WHS YUG +WATER TYS ABUTS BLS CRS IFR IOS LGS LIS ROM/WC SAR SIC SJT SLR TAR +LAND UBU ABUTS AUT HEL LBU LOT SWA +LAND UDM ABUTS ATI BAS BUL KOR MRD STB +COAST UJJ ABUTS ARS IND KNJ MAN MAS RAS SHA +COAST UPP ABUTS FIS GOT LES nor SAA/SC vik +COAST URG ABUTS ALI BUK GHU HER KHO KYK NKS SKS +LAND UYG ABUTS AMD CHA CHN ORB SAM SOG +COAST VAL ABUTS BLS COR GRA SJT SPM TOL ZAR +COAST VAR ABUTS CHL CIB KNJ NEP PAL +COAST vel ABUTS ABS BRE JLN POM SAX SGS +COAST VEL/NC ABUTS ABS JLN POM +COAST VEL/WC ABUTS BRE JLN SGS +COAST VIK ABUTS got KAT NOR SCA upp +COAST VLA ABUTS KIE ONO PEC/WC THR VOL WES +LAND VOL ABUTS DRE KIE MAZ MOR ONO POL VLA +LAND VYA ABUTS BUL CHE KIE KRI MRD SEV SMO +LAND WAL ABUTS ASS BSK KUS NIO SAN SIJ TIR +WATER WES ABUTS BUC CHS CON EES PEC/WC THR VLA +WATER WHS ABUTS BJA ICS KOR TUN YUG +COAST WLS ABUTS JOR/WC WSS WSX +WATER WSS ABUTS CLD DUB ICS JOR/WC NOS WLS WSX +COAST WSX ABUTS BRC JOR/EC NGS NOS SGS WLS WSS +WATER WTS ABUTS ATS SOS SWO TAS TKA +COAST YAN ABUTS BLH/SC CHA CHN HEN KHI YES +COAST YEM ABUTS ARS GAD MEC NEF OMA SRS +WATER YES ABUTS BLH/SC EAS HEN NAN SIL YAN +COAST YUG ABUTS BAS kip KOR KYR STB TUN WHS +LAND ZAR ABUTS AST PAM SAL SPM TOL VAL +LAND ZAW ABUTS ASS DAJ FAZ +COAST ZEI ABUTS ADU GAD MAL ROH +COAST ZIM ABUTS GOS kon LUB MES NAM RHA + diff --git a/diplomacy/maps/modern.map b/diplomacy/maps/modern.map new file mode 100644 index 0000000..d958553 --- /dev/null +++ b/diplomacy/maps/modern.map @@ -0,0 +1,351 @@ +BEGIN SPRING 1994 MOVEMENT +RULES BUILD_ANY + +BRITAIN (BRITISH) LON LIV EDI GIB +F LON +F LIV +F EDI +F GIB + +EGYPT (EGYPTIAN) CAI ALE ASW +F CAI +F ALE +A ASW + +FRANCE (FRENCH) PAR BOR MAR LYO +A PAR +F BOR +A MAR +A LYO + +GERMANY (GERMAN) HAM BER FRA MUN +F HAM +F BER +A FRA +A MUN + +ITALY (ITALIAN) MIL VEN ROM NAP +A MIL +F VEN +A ROM +F NAP + +POLAND (POLISH) WAR GDA KRA +A WAR +F GDA +A KRA + +RUSSIA (RUSSIAN) MOS GOR STP MUR ROS +A MOS +A GOR +F STP +F MUR +F ROS + +SPAIN (SPANISH) BAR MAD SVE +F BAR +A MAD +A SVE + +TURKEY (TURKISH) ADA IZM IST ANK +A ADA +F IZM +A IST +F ANK + +UKRAINE (UKRAINIAN) SEV KHA KIE ODE +F SEV +A KHA +A KIE +A ODE + +UNOWNED IRE MOR NWY ISR LIB BEL MON SWI AUS DEN HOL CRO SER +UNOWNED TUN BIE CZE LIT SWE POR BUL GEO GRE IRN RUM HUN SAU + +Adana = ada +Adriatic Sea = adr adrs adriatic +Aegean Sea = aeg aegs aegean +Albania = alb +Alexandria = ale +Algeria = alg +Alsace = als +Anatolia = ana +Andalusia = adl and? andalucia +Ankara = ank +Apulia = apu +Arabian Sea = ara aras arabian +Armenia = arm +Aswan = asw +Austria = aus aut +Auvergne = auv +Arctic Ocean = arc arctic +Azerbaijan = aze +Baltic Sea = bal bals baltic +Bay of Biscay = bis bob bay biscay +Barcelona = bar barc +Barents Sea = brn bars brnts barents +Belgium = bel +Berlin = ber +Bielorussia = bie +Bordeaux = bor +Bornholm Sea = bhm bors born bornholm +Bosnia = bos +Brittany = bri +Bulgaria = bul +Cairo = cai +Caspian Sea = cas cass caspian +Caucasus = cau +Central Russian Plateau = crp cen pla +Clyde = cly +Croatia = cro +Czech Republic = cze +Denmark = den +Donbas = don donetsk +Eastern Black Sea = ebs ebl ebla eblack +Eastern Mediterranean = eme emed +Eastern Sahara = esa esah +Edinburgh = edi +English Channel = eng english cha channel +Estonia = est +Finland = fin +Frankfurt = fra +Gdansk = gda danzig dan +Georgia = geo +Gibraltar = gib +Gorky = gor +Greece = gre +Gulf of Bothnia = gob bot bothnia gbothnia +Gulf of Lyon = gol glyon +Hamburg = ham +Heligoland Bight = hel heligoland +Holland = hol +Hungary = hun +Iceland = ice +Ionian Sea = ion ions ionian +Irak = irk irq iraq +Iran (north coast) = irn/nc +Iran (south coast) = irn/sc +Iran = irn +Ireland = ire eire eir +Irish Sea = iri iris irish +Israel = isr +Istanbul = ist constantinople con +Izmir = izm +Jordan = jor +Kazakhstan = kaz +Kharkov = kha +Kiev = kie +Krakow = kra +Lapland = lap +Latvia = lat +Libya = lib +Libyan Sea = lbn libs libyan +Ligurian Sea = lig ligs ligurian +Lithuania = lit +Liverpool = liv lpl livp +London = lon +Lyon = lyo +Macedonia = mac skopje sko +Madrid = mad +Maltese Sea = mal mals matlese +Marseilles = mar +Mid Atlantic Ocean = mid mao mat +Milan = mil +Moldavia = mol +Monaco = mon +Morocco = mor +Moscow = mos +Munich = mun +Murmansk = mur +Naples = nap +Navarra = nav +North Atlantic Ocean = nao nat +North Sea = nth north +Norway = nwy nor +Norwegian Sea = nwg nors norwegian +Odessa = ode +Paris = par +Persian Gulf = per perg persian +Picardy = pic +Piedmont = pie +Podolia = pod +Portugal = por +Prussia = pru +Red Sea = red reds +Rome = rom +Rostov = ros +Ruhr = ruh +Rumania = rum +Saudi Arabia = sau saudi +Saxony = sax +Serbia = ser +Sevastopol = sev seva +Seville = sve sevi svl +Siberia = sib +Silesia = sil silecia +Sinai = sin +Skagerrak = ska +Slovakia = slk slo +South Atlantic Ocean = sao sat sou south +St-Petersburg = stp leningrad len +Straits of Gibraltar = sog sgib str straits sea+of+gibraltar +Sweden = swe +Switzerland = swi sui +Syria = syr +Tunisia = tun tunis +Tuscany = tus +Tyrrhenian Sea = tyr tyrs tys tyrrhenian +Urals = ura +Venice = ven +Volga = vol +Wales = wal +Warsaw = war +Western Black Sea = wbs wbl wbla wblack +Western Mediterranean = wme wmed +Western Sahara = wsa wsah +White Sea = whi white +Yorkshire = yor york + +COAST ADA ABUTS ANA ank ARM irk irn SYR EME +WATER ADR ABUTS ALB APU CRO ION SER VEN +WATER AEG ABUTS EME GRE ION IST IZM +COAST ALB ABUTS GRE MAC SER ADR ION +COAST ALE ABUTS asw CAI ESA EME LBN +COAST ALG ABUTS lib MOR TUN WSA SOG WME +LAND ALS ABUTS BEL LYO MUN PAR PIC RUH SWI +COAST ANA ABUTS ADA ank IZM EME +COAST ADL ABUTS BAR GIB MAD sve SOG WME +COAST ANK ABUTS ada ana ARM GEO IST izm EBS WBS +COAST APU ABUTS NAP rom VEN ADR ION +WATER ARA ABUTS IRN/SC PER RED SAU +WATER ARC ABUTS BRN ICE NAO NWG +LAND ARM ABUTS ADA ANK AZE GEO IRN +COAST ASW ABUTS ale CAI esa RED +LAND AUS ABUTS CRO CZE HUN MIL MUN SAX SLK SWI VEN +COAST AUV ABUTS BAR bor LYO MAR nav PAR GOL +COAST AZE ABUTS ARM CAU geo CAS IRN/NC +WATER BAL ABUTS BHM GDA GOB LAT LIT PRU SWE +COAST BAR ABUTS ADL AUV MAD nav GOL WME +COAST BEL ABUTS ALS HOL PIC RUH ENG NTH +COAST BER ABUTS FRA HAM PRU SAX SIL BHM +WATER BHM ABUTS BAL BER DEN HAM PRU SWE +LAND BIE ABUTS CRP KIE KRA LAT LIT MOS POD WAR +WATER BIS ABUTS BOR BRI ENG MID NAV +COAST BOR ABUTS auv BRI NAV PAR BIS +LAND BOS ABUTS CRO SER +COAST BRI ABUTS BOR PAR PIC BIS ENG +WATER BRN ABUTS ARC LAP MUR NWG URA WHI +COAST BUL ABUTS gre IST MAC RUM ser WBS +COAST CAI ABUTS ALE ASW ISR SIN EME RED +WATER CAS ABUTS AZE CAU IRN/NC KAZ VOL +COAST CAU ABUTS AZE geo ros VOL CAS +COAST CLY ABUTS EDI LIV NAO NWG +COAST CRO ABUTS AUS BOS HUN SER VEN ADR +LAND CRP ABUTS BIE KHA KIE MOS VOL +LAND CZE ABUTS AUS KRA SAX SIL SLK +COAST DEN ABUTS HAM SWE BHM HEL NTH SKA +COAST DON ABUTS KHA KIE ROS SEV vol EBS +WATER EBS ABUTS ANK DON GEO ROS SEV WBS +COAST EDI ABUTS CLY liv YOR NTH NWG +WATER EME ABUTS ADA AEG ALE ANA CAI ION ISR IZM LBN SYR +WATER ENG ABUTS BEL BIS BRI IRI LON MID NTH PIC WAL +COAST ESA ABUTS ALE asw LIB LBN +COAST EST ABUTS LAT STP GOB +COAST FIN ABUTS lap mur STP SWE GOB +LAND FRA ABUTS BER HAM MUN RUH SAX +COAST GDA ABUTS LIT PRU WAR BAL +COAST GEO ABUTS ANK ARM aze cau ROS EBS +COAST GIB ABUTS ADL SVE SAO SOG +WATER GOB ABUTS BAL EST FIN LAT STP SWE +WATER GOL ABUTS AUV BAR LIG MAR TYR WME +LAND GOR ABUTS MOS MUR STP URA VOL +COAST GRE ABUTS ALB bul IST MAC AEG ION +COAST HAM ABUTS BER DEN FRA HOL RUH BHM HEL +WATER HEL ABUTS DEN HAM HOL NTH +COAST HOL ABUTS BEL HAM RUH HEL NTH +LAND HUN ABUTS AUS CRO POD RUM SER SLK +COAST ICE ABUTS ARC NAO NWG +WATER ION ABUTS ADR AEG ALB APU EME GRE LBN MAL NAP +COAST IRE ABUTS IRI NAO +WATER IRI ABUTS ENG IRE LIV MID NAO WAL +COAST IRK ABUTS ada jor SAU syr IRN/SC PER +COAST IRN/NC ABUTS AZE CAS KAZ +COAST IRN/SC ABUTS ARA IRK PER +COAST irn ABUTS ADA ARM AZE IRK KAZ PER ARA CAS +COAST ISR ABUTS CAI jor sin SYR EME +COAST IST ABUTS ANK BUL GRE IZM AEG WBS +COAST IZM ABUTS ANA ank IST AEG EME +COAST JOR ABUTS irk isr SAU SIN syr RED +COAST KAZ ABUTS SIB VOL CAS IRN/NC +LAND KHA ABUTS CRP DON KIE VOL +LAND KIE ABUTS BIE CRP DON KHA ODE POD SEV +LAND KRA ABUTS BIE CZE POD SIL SLK WAR +COAST LAP ABUTS fin MUR NWY swe BRN NWG +COAST LAT ABUTS BIE EST LIT MOS stp BAL GOB +WATER LBN ABUTS ALE EME ESA ION LIB MAL +COAST LIB ABUTS alg ESA TUN WSA LBN MAL +WATER LIG ABUTS GOL MAR MON PIE ROM TUS TYR +COAST LIT ABUTS BIE GDA LAT WAR BAL +COAST LIV ABUTS CLY edi WAL yor IRI NAO +COAST LON ABUTS WAL YOR ENG NTH +LAND LYO ABUTS ALS AUV MAR PAR SWI +LAND MAC ABUTS ALB BUL GRE SER +LAND MAD ABUTS ADL BAR NAV POR SVE +WATER MAL ABUTS ION LBN LIB NAP TUN TYR +COAST MAR ABUTS AUV LYO MON pie SWI GOL LIG +WATER MID ABUTS BIS ENG IRI NAO NAV POR SAO +LAND MIL ABUTS AUS PIE SWI TUS VEN +LAND MOL ABUTS ODE POD RUM +COAST MON ABUTS MAR PIE LIG +COAST MOR ABUTS ALG WSA SAO SOG +LAND MOS ABUTS BIE CRP GOR LAT STP VOL +LAND MUN ABUTS ALS AUS FRA RUH SAX SWI +COAST MUR ABUTS fin GOR LAP stp URA BRN WHI +WATER NAO ABUTS ARC CLY ICE IRE IRI LIV MID NWG +COAST NAP ABUTS APU ROM ION MAL TYR +COAST NAV ABUTS auv bar BIS BOR MAD POR MID +COAST NWY ABUTS LAP SWE NTH NWG SKA +WATER NTH ABUTS BEL DEN EDI ENG HEL HOL LON NWY NWG SKA YOR +WATER NWG ABUTS ARC BRN CLY EDI ICE LAP NAO NWY NTH +COAST ODE ABUTS KIE MOL POD RUM SEV WBS +LAND PAR ABUTS ALS AUV BOR BRI LYO PIC +WATER PER ABUTS ARA IRK IRN/SC SAU +COAST PIC ABUTS ALS BEL BRI PAR ENG +COAST PIE ABUTS mar MIL MON SWI TUS LIG +LAND POD ABUTS BIE HUN KIE KRA MOL ODE RUM SLK +COAST POR ABUTS MAD NAV SVE MID SAO +COAST PRU ABUTS BER GDA SIL WAR BAL BHM +WATER RED ABUTS ARA ASW CAI JOR SAU SIN +COAST ROM ABUTS apu NAP TUS ven LIG TYR +COAST ROS ABUTS cau DON GEO VOL EBS +LAND RUH ABUTS ALS BEL FRA HAM HOL MUN +COAST RUM ABUTS BUL HUN MOL ODE POD ser WBS +WATER SAO ABUTS GIB MID MOR POR SOG SVE +COAST SAU ABUTS IRK JOR ARA PER RED +LAND SAX ABUTS AUS BER CZE FRA MUN SIL +COAST SER ABUTS ALB BOS bul CRO HUN MAC rum ADR +COAST SEV ABUTS DON KIE ODE EBS WBS +LAND SIB ABUTS KAZ URA VOL +LAND SIL ABUTS BER CZE KRA PRU SAX WAR +COAST SIN ABUTS CAI isr JOR RED +WATER SKA ABUTS DEN NWY NTH SWE +LAND SLK ABUTS AUS CZE HUN KRA POD +COAST STP ABUTS EST FIN GOR lat MOS mur GOB +WATER SOG ABUTS ALG ADL GIB MOR SAO WME +COAST SVE ABUTS adl GIB MAD POR SAO +COAST SWE ABUTS DEN FIN lap NWY BAL BHM GOB SKA +LAND SWI ABUTS ALS AUS LYO MAR MIL MUN PIE +COAST SYR ABUTS ADA irk ISR jor EME +COAST TUN ABUTS ALG LIB MAL TYR WME +COAST TUS ABUTS MIL PIE ROM ven LIG +WATER TYR ABUTS GOL LIG MAL NAP ROM TUN WME +COAST URA ABUTS GOR MUR SIB vol BRN WHI +COAST VEN ABUTS APU AUS CRO MIL rom tus ADR +COAST VOL ABUTS CAU CRP don GOR KAZ KHA MOS ROS SIB ura CAS +COAST WAL ABUTS LIV LON yor ENG IRI +LAND WAR ABUTS BIE GDA KRA LIT PRU SIL +WATER WBS ABUTS ANK BUL EBS IST ODE RUM SEV +WATER WHI ABUTS BRN MUR URA +WATER WME ABUTS ALG ADL BAR GOL SOG TUN TYR +LAND WSA ABUTS ALG LIB MOR +COAST YOR ABUTS EDI liv LON wal NTH diff --git a/diplomacy/maps/standard.map b/diplomacy/maps/standard.map new file mode 100644 index 0000000..620fa3a --- /dev/null +++ b/diplomacy/maps/standard.map @@ -0,0 +1,213 @@ +BEGIN SPRING 1901 MOVEMENT + +AUSTRIA (AUSTRIAN) BUD TRI VIE +A BUD +A VIE +F TRI + +ENGLAND (ENGLISH) EDI LON LVP +F EDI +F LON +A LVP + +FRANCE (FRENCH) BRE MAR PAR +F BRE +A MAR +A PAR + +GERMANY (GERMAN) BER KIE MUN +F KIE +A BER +A MUN + +ITALY (ITALIAN) NAP ROM VEN +F NAP +A ROM +A VEN + +RUSSIA (RUSSIAN) MOS SEV STP WAR +A WAR +A MOS +F SEV +F STP/SC + +TURKEY (TURKISH) ANK CON SMY +F ANK +A CON +A SMY + +UNOWNED BEL BUL DEN GRE HOL NWY POR RUM SER SPA SWE TUN + +Adriatic Sea = adr adriatic +Aegean Sea = aeg aegean +Albania = alb +Ankara = ank +Apulia = apu +Armenia = arm +Baltic Sea = bal baltic +Barents Sea = bar barents +Belgium = bel +Berlin = ber berl +Black Sea = bla black bs +Bohemia = boh +Brest = bre +Budapest = bud +Bulgaria (east coast) = bul/ec bul(ec) bulgaria/ec bulgaria(ec) bul/nc bul(nc) bulgaria/nc bulgaria(nc) +Bulgaria (south coast) = bul/sc bul(sc) bulgaria/sc bulgaria(sc) +Bulgaria = bul bulg +Burgundy = bur burg burgandy +Clyde = cly +Constantinople = con +Denmark = den +Eastern Mediterranean = eas emed east eastern eastmed ems eme emd eas+med +Edinburgh = edi +English Channel = eng channel ech eng+ch +Finland = fin +Galicia = gal galacia +Gascony = gas gasc +Greece = gre +Gulf of Lyon = lyo gol gulfofl lyon gulf+of+lyons lyons +Gulf of Bothnia = bot gob both gulfofb bothnia +Helgoland Bight = hel helgoland heligoland heligoland+bight helg +Holland = hol +Ionian Sea = ion ionian +Irish Sea = iri irish irs +Kiel = kie +Liverpool = lvp livp lpl +Livonia = lvn livo lvo lva liv livon +London = lon +Marseilles = mar mars +Mid-Atlantic Ocean = mao midatlanticocean midatlantic mid mat +Moscow = mos +Munich = mun +Naples = nap +North Atlantic Ocean = nao nat na +North Africa = naf nora n+afr +North Sea = nth norsea nts ns +Norway = nwy nor norw +Norwegian Sea = nwg norwegian norwsea nrg norg +Paris = par +Picardy = pic +Piedmont = pie pid +Portugal = por port +Prussia = pru prus +Rome = rom +Ruhr = ruh +Rumania = rum romania +Serbia = ser serb +Sevastopol = sev sevastapol sevast seva +Silesia = sil +Skagerrak = ska skagerrack skag +Smyrna = smy +Spain (north coast) = spa/nc spa(nc) spain/nc spain(nc) spn/nc spn(nc) spa+(north+coast) +Spain (south coast) = spa/sc spa(sc) spain/sc spain(sc) spn/sc spn(sc) spa+(south+coast) spa/wc +Spain = spa spn +St Petersburg (north coast) = stp/nc stp(nc) stpete/nc stpete(nc) st.+petersburg/nc stp+(north+coast) +St Petersburg (south coast) = stp/sc stp(sc) stpete/sc stpete(sc) st.+petersburg/sc stp+(south+coast) +St Petersburg = stp stpete st.+petersburg st.petersburg +Sweden = swe +Syria = syr +Trieste = tri +Tunis = tun +Tuscany = tus tusc +Tyrolia = tyr tyl trl tyo tyrol +Tyrrhenian Sea = tys tyrr tyrrhenian tyn tyh tyrhh +Ukraine = ukr +Venice = ven +Vienna = vie vien +Wales = wal +Warsaw = war +Western Mediterranean = wes wmed west western westmed wms wme wmd west+med wes+med western+med +Yorkshire = yor york yonkers + +# ---------------------------------------------------------------- +# Location adjacency information, keyed off the official dpjudge +# name of the location. Here, upper- and lower-case IS important. +# ---------------------------------------------------------------- +WATER ADR ABUTS ALB APU ION TRI VEN +WATER AEG ABUTS BUL/SC CON EAS GRE ION SMY +COAST ALB ABUTS ADR GRE ION SER TRI +COAST ANK ABUTS ARM BLA CON smy +COAST APU ABUTS ADR ION NAP rom VEN +COAST ARM ABUTS ANK BLA SEV smy syr +WATER BAL ABUTS BER BOT DEN LVN KIE PRU SWE +WATER BAR ABUTS NWY NWG STP/NC +COAST BEL ABUTS BUR ENG HOL NTH PIC RUH +COAST BER ABUTS BAL KIE MUN PRU SIL +WATER BLA ABUTS ANK ARM BUL/EC CON RUM SEV +LAND BOH ABUTS GAL MUN SIL TYR VIE +WATER BOT ABUTS BAL FIN LVN STP/SC SWE +COAST BRE ABUTS ENG GAS MAO PAR PIC +LAND BUD ABUTS GAL RUM SER TRI VIE +COAST BUL/EC ABUTS BLA CON RUM +COAST BUL/SC ABUTS AEG CON GRE +COAST bul ABUTS AEG BLA CON GRE RUM SER +LAND BUR ABUTS BEL GAS RUH MAR MUN PAR PIC SWI +COAST CLY ABUTS EDI LVP NAO NWG +COAST CON ABUTS AEG BUL/EC BUL/SC BLA ANK SMY +COAST DEN ABUTS BAL HEL KIE NTH SKA SWE +WATER EAS ABUTS AEG ION SMY SYR +COAST EDI ABUTS CLY lvp NTH NWG YOR +WATER ENG ABUTS BEL BRE IRI LON MAO NTH PIC WAL +COAST FIN ABUTS BOT nwy STP/SC SWE +LAND GAL ABUTS BOH BUD RUM SIL UKR VIE WAR +COAST GAS ABUTS BUR BRE MAO mar PAR SPA/NC +COAST GRE ABUTS AEG ALB BUL/SC ION SER +WATER HEL ABUTS DEN HOL KIE NTH +COAST HOL ABUTS BEL HEL KIE NTH RUH +WATER ION ABUTS ADR AEG ALB APU EAS GRE NAP TUN TYS +WATER IRI ABUTS ENG LVP MAO NAO WAL +COAST KIE ABUTS BAL BER DEN HEL HOL MUN RUH +COAST LON ABUTS ENG NTH YOR WAL +COAST LVN ABUTS BAL BOT MOS PRU STP/SC WAR +COAST LVP ABUTS CLY edi IRI NAO WAL yor +WATER LYO ABUTS MAR PIE SPA/SC TUS TYS WES +WATER MAO ABUTS BRE ENG GAS IRI NAF NAO POR SPA/NC SPA/SC WES +COAST MAR ABUTS BUR gas LYO PIE SPA/SC SWI +LAND MOS ABUTS LVN SEV STP UKR WAR +LAND MUN ABUTS BER BOH BUR KIE RUH SIL TYR SWI +COAST NAF ABUTS MAO TUN WES +WATER NAO ABUTS CLY IRI LVP MAO NWG +COAST NAP ABUTS APU ION ROM TYS +COAST NWY ABUTS BAR fin NTH NWG SKA STP/NC SWE +WATER NTH ABUTS BEL DEN EDI ENG LON HEL HOL NWY NWG SKA YOR +WATER NWG ABUTS BAR CLY EDI NAO NWY NTH +LAND PAR ABUTS BUR BRE GAS PIC +COAST PIC ABUTS BEL BRE BUR ENG PAR +COAST PIE ABUTS LYO MAR TUS TYR ven SWI +COAST POR ABUTS MAO SPA/NC SPA/SC +COAST PRU ABUTS BAL BER LVN SIL WAR +COAST ROM ABUTS apu NAP TUS TYS ven +LAND RUH ABUTS BEL BUR HOL KIE MUN +COAST RUM ABUTS BLA BUD BUL/EC GAL SER SEV UKR +LAND SER ABUTS ALB BUD BUL GRE RUM TRI +COAST SEV ABUTS ARM BLA MOS RUM UKR +LAND SIL ABUTS BER BOH GAL MUN PRU WAR +WATER SKA ABUTS DEN NWY NTH SWE +COAST SMY ABUTS AEG ank arm CON EAS SYR +COAST SPA/NC ABUTS GAS MAO POR +COAST SPA/SC ABUTS LYO MAO MAR POR WES +COAST spa ABUTS GAS LYO MAO MAR POR WES +COAST STP/NC ABUTS BAR NWY +COAST STP/SC ABUTS BOT FIN LVN +COAST stp ABUTS BAR BOT FIN LVN MOS NWY +COAST SWE ABUTS BAL BOT DEN FIN NWY SKA +COAST SYR ABUTS arm EAS SMY +COAST TRI ABUTS ADR ALB BUD SER TYR VEN VIE +COAST TUN ABUTS ION NAF TYS WES +COAST TUS ABUTS LYO PIE ROM TYS ven +LAND TYR ABUTS BOH MUN PIE TRI VEN VIE SWI +WATER TYS ABUTS ION LYO ROM NAP TUN TUS WES +LAND UKR ABUTS GAL MOS RUM SEV WAR +COAST VEN ABUTS ADR APU pie rom TRI tus TYR +LAND VIE ABUTS BOH BUD GAL TRI TYR +COAST WAL ABUTS ENG IRI LON LVP yor +LAND WAR ABUTS GAL LVN MOS PRU SIL UKR +WATER WES ABUTS MAO LYO NAF SPA/SC TUN TYS +COAST YOR ABUTS EDI LON lvp NTH wal + +# ------------------------ +# Switzerland (impassable) +# ------------------------ +Switzerland = swi +SHUT SWI ABUTS MAR BUR MUN TYR PIE diff --git a/diplomacy/maps/standard_france_austria.map b/diplomacy/maps/standard_france_austria.map new file mode 100644 index 0000000..60456e7 --- /dev/null +++ b/diplomacy/maps/standard_france_austria.map @@ -0,0 +1,6 @@ +# WebDiplomacy.net - Classic France - Austria +MAP standard.map +UNPLAYED ENGLAND, GERMANY, ITALY, RUSSIA, TURKEY + +UNOWNED EDI LON LVP BER KIE MUN NAP ROM VEN MOS SEV STP WAR ANK CON SMY +UNOWNED BEL BUL DEN GRE HOL NWY POR RUM SER SPA SWE TUN diff --git a/diplomacy/maps/standard_germany_italy.map b/diplomacy/maps/standard_germany_italy.map new file mode 100644 index 0000000..8f7320d --- /dev/null +++ b/diplomacy/maps/standard_germany_italy.map @@ -0,0 +1,6 @@ +# WebDiplomacy.net - Classic Germany - Italy +MAP standard.map +UNPLAYED AUSTRIA, FRANCE, ENGLAND, RUSSIA, TURKEY + +UNOWNED BUD TRI VIE EDI LON LVP BRE MAR PAR MOS SEV STP WAR ANK CON SMY +UNOWNED BEL BUL DEN GRE HOL NWY POR RUM SER SPA SWE TUN
\ No newline at end of file diff --git a/diplomacy/maps/svg/standard.svg b/diplomacy/maps/svg/standard.svg new file mode 100644 index 0000000..b0b43c3 --- /dev/null +++ b/diplomacy/maps/svg/standard.svg @@ -0,0 +1,976 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "svg.dtd"> +<!-- ========================================================================================== --> +<!-- Detailed Standard Map --> +<!-- background (and quite nice) map bitmap by J. Fatula III --> +<!-- SVG by Zach DelProposto --> +<!-- Copyright jDip - GPL License --> +<!-- ========================================================================================== --> +<svg color-rendering="optimizeQuality" height="680px" preserveAspectRatio="xMinYMin" version="1.0" viewBox="0 0 1835 1360" width="918px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:jdipNS="svg.dtd"> + + <jdipNS:DISPLAY> + <jdipNS:ZOOM min="5" max="2200" factor="1.2"/> + <jdipNS:LABELS brief="true" full="true"/> + </jdipNS:DISPLAY> + + <jdipNS:ORDERDRAWING> + <jdipNS:POWERCOLORS> + <jdipNS:POWERCOLOR power="austria" color="#c48f85"/> + <jdipNS:POWERCOLOR power="england" color="darkviolet"/> + <jdipNS:POWERCOLOR power="france" color="royalblue"/> + <jdipNS:POWERCOLOR power="germany" color="#a08a75"/> + <jdipNS:POWERCOLOR power="italy" color="forestgreen"/> + <jdipNS:POWERCOLOR power="russia" color="#757d91"/> + <jdipNS:POWERCOLOR power="turkey" color="#b9a61c"/> + </jdipNS:POWERCOLORS> + + <jdipNS:SYMBOLSIZE name="Fleet" width="40" height="40"/> + <jdipNS:SYMBOLSIZE name="Army" width="40" height="40"/> + <jdipNS:SYMBOLSIZE name="Wing" width="40" height="40"/> + <jdipNS:SYMBOLSIZE name="DislodgedFleet" width="40" height="40"/> + <jdipNS:SYMBOLSIZE name="DislodgedArmy" width="40" height="40"/> + <jdipNS:SYMBOLSIZE name="DislodgedWing" width="40" height="40"/> + <jdipNS:SYMBOLSIZE name="FailedOrder" width="30" height="30"/> + <jdipNS:SYMBOLSIZE name="SupplyCenter" width="20" height="20"/> + <jdipNS:SYMBOLSIZE name="BuildUnit" width="60" height="60"/> + <jdipNS:SYMBOLSIZE name="RemoveUnit" width="50" height="50"/> + <jdipNS:SYMBOLSIZE name="WaivedBuild" width="40" height="40"/> + + <jdipNS:BUILD deltaRadius="0"/> + <jdipNS:REMOVE deltaRadius="5"/> + <jdipNS:DISBAND deltaRadius="5"/> + <jdipNS:WAIVE deltaRadius="0"/> + <jdipNS:HOLD deltaRadius="5" strokeCSSStyle="varwidthorder" highlightOffset="0" highlightCSSClass="varwidthshadow" widths="6,9,12,18" shadowWidths="10,15,20,25"/> + <jdipNS:MOVE deltaRadius="5" strokeCSSStyle="varwidthorder" markerID="arrow" highlightOffset="0" highlightCSSClass="varwidthshadow" widths="6,9,12,18" shadowWidths="10,15,20,25"/> + <jdipNS:RETREAT deltaRadius="5" strokeCSSStyle="defaultorder" markerID="arrow" highlightOffset="0" highlightCSSClass="shadoworder"/> + <jdipNS:SUPPORT deltaRadius="10" strokeCSSStyle="supportorder" markerID="arrow" highlightOffset="0" highlightCSSClass="shadowdash"/> + <jdipNS:CONVOY deltaRadius="10" strokeCSSStyle="convoyorder" markerID="arrow" highlightOffset="0" highlightCSSClass="shadowdash"/> + </jdipNS:ORDERDRAWING> + + <jdipNS:PROVINCE_DATA> + <jdipNS:PROVINCE name="adr"> + <jdipNS:UNIT x="805" y="1058"/> + <jdipNS:DISLODGED_UNIT x="792.5" y="1045.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="aeg"> + <jdipNS:UNIT x="1055" y="1240"/> + <jdipNS:DISLODGED_UNIT x="1042.5" y="1227.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="alb"> + <jdipNS:UNIT x="918" y="1123"/> + <jdipNS:DISLODGED_UNIT x="905.5" y="1110.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="ank"> + <jdipNS:UNIT x="1313" y="1120"/> + <jdipNS:DISLODGED_UNIT x="1300.5" y="1107.5"/> + <jdipNS:SUPPLY_CENTER x="1271" y="1144"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="apu"> + <jdipNS:UNIT x="803" y="1116"/> + <jdipNS:DISLODGED_UNIT x="790.5" y="1103.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="arm"> + <jdipNS:UNIT x="1496" y="1100"/> + <jdipNS:DISLODGED_UNIT x="1483.5" y="1087.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bal"> + <jdipNS:UNIT x="890" y="620"/> + <jdipNS:DISLODGED_UNIT x="877.5" y="607.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bar"> + <jdipNS:UNIT x="1174" y="83"/> + <jdipNS:DISLODGED_UNIT x="1161.5" y="70.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bel"> + <jdipNS:UNIT x="573" y="763"/> + <jdipNS:DISLODGED_UNIT x="560.5" y="750.5"/> + <jdipNS:SUPPLY_CENTER x="575" y="755"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="ber"> + <jdipNS:UNIT x="783" y="700"/> + <jdipNS:DISLODGED_UNIT x="770.5" y="687.5"/> + <jdipNS:SUPPLY_CENTER x="783" y="736"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bla"> + <jdipNS:UNIT x="1245" y="1010"/> + <jdipNS:DISLODGED_UNIT x="1232.5" y="997.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="boh"> + <jdipNS:UNIT x="818" y="824"/> + <jdipNS:DISLODGED_UNIT x="805.5" y="811.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bot"> + <jdipNS:UNIT x="953" y="495"/> + <jdipNS:DISLODGED_UNIT x="940.5" y="482.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bre"> + <jdipNS:UNIT x="416" y="829"/> + <jdipNS:DISLODGED_UNIT x="403.5" y="816.5"/> + <jdipNS:SUPPLY_CENTER x="369" y="799"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bud"> + <jdipNS:UNIT x="962" y="914"/> + <jdipNS:DISLODGED_UNIT x="949.5" y="901.5"/> + <jdipNS:SUPPLY_CENTER x="900" y="916"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bul"> + <jdipNS:UNIT x="1060" y="1078"/> + <jdipNS:DISLODGED_UNIT x="1047.5" y="1065.5"/> + <jdipNS:SUPPLY_CENTER x="1013" y="1078"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bur"> + <jdipNS:UNIT x="571" y="881"/> + <jdipNS:DISLODGED_UNIT x="558.5" y="868.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="cly"> + <jdipNS:UNIT x="448" y="502"/> + <jdipNS:DISLODGED_UNIT x="435.5" y="489.5"/> + <jdipNS:SUPPLY_CENTER x="446" y="480"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="con"> + <jdipNS:UNIT x="1157" y="1147"/> + <jdipNS:DISLODGED_UNIT x="1144.5" y="1134.5"/> + <jdipNS:SUPPLY_CENTER x="1141" y="1119"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="den"> + <jdipNS:UNIT x="715" y="597"/> + <jdipNS:DISLODGED_UNIT x="702.5" y="584.5"/> + <jdipNS:SUPPLY_CENTER x="767" y="626"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="eas"> + <jdipNS:UNIT x="1230" y="1321"/> + <jdipNS:DISLODGED_UNIT x="1217.5" y="1308.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="edi"> + <jdipNS:UNIT x="485" y="524"/> + <jdipNS:DISLODGED_UNIT x="472.5" y="511.5"/> + <jdipNS:SUPPLY_CENTER x="489" y="565"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="eng"> + <jdipNS:UNIT x="406" y="761"/> + <jdipNS:DISLODGED_UNIT x="393.5" y="748.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="fin"> + <jdipNS:UNIT x="1000" y="390"/> + <jdipNS:DISLODGED_UNIT x="987.5" y="377.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="gal"> + <jdipNS:UNIT x="1011" y="841"/> + <jdipNS:DISLODGED_UNIT x="998.5" y="828.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="gas"> + <jdipNS:UNIT x="434" y="922"/> + <jdipNS:DISLODGED_UNIT x="421.5" y="909.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="gol"> + <jdipNS:UNIT x="556" y="1060"/> + <jdipNS:DISLODGED_UNIT x="543.5" y="1047.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="gre"> + <jdipNS:UNIT x="978" y="1200"/> + <jdipNS:DISLODGED_UNIT x="965.5" y="1187.5"/> + <jdipNS:SUPPLY_CENTER x="1023" y="1237"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="hel"> + <jdipNS:UNIT x="663" y="641"/> + <jdipNS:DISLODGED_UNIT x="650.5" y="628.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="hol"> + <jdipNS:UNIT x="608" y="721"/> + <jdipNS:DISLODGED_UNIT x="595.5" y="708.5"/> + <jdipNS:SUPPLY_CENTER x="630" y="692"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="ion"> + <jdipNS:UNIT x="858" y="1296"/> + <jdipNS:DISLODGED_UNIT x="845.5" y="1283.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="iri"> + <jdipNS:UNIT x="347" y="671"/> + <jdipNS:DISLODGED_UNIT x="334.5" y="658.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="kie"> + <jdipNS:UNIT x="695" y="711"/> + <jdipNS:DISLODGED_UNIT x="682.5" y="698.5"/> + <jdipNS:SUPPLY_CENTER x="724" y="685"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="lon"> + <jdipNS:UNIT x="500" y="685"/> + <jdipNS:DISLODGED_UNIT x="487.5" y="672.5"/> + <jdipNS:SUPPLY_CENTER x="489" y="717"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="lvn"> + <jdipNS:UNIT x="1037" y="577"/> + <jdipNS:DISLODGED_UNIT x="1024.5" y="564.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="lvp"> + <jdipNS:UNIT x="462" y="586"/> + <jdipNS:DISLODGED_UNIT x="449.5" y="573.5"/> + <jdipNS:SUPPLY_CENTER x="458" y="638"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="lyo"> + <jdipNS:UNIT x="525.8" y="1065"/> + <jdipNS:DISLODGED_UNIT x="513.3" y="1052.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="mar"> + <jdipNS:UNIT x="536" y="985"/> + <jdipNS:DISLODGED_UNIT x="523.5" y="972.5"/> + <jdipNS:SUPPLY_CENTER x="562" y="1018"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="mao"> + <jdipNS:UNIT x="153.3" y="845.3"/> + <jdipNS:DISLODGED_UNIT x="140.8" y="832.8"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="mid"> + <jdipNS:UNIT x="126" y="902"/> + <jdipNS:DISLODGED_UNIT x="113.5" y="889.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="mos"> + <jdipNS:UNIT x="1212" y="600"/> + <jdipNS:DISLODGED_UNIT x="1199.5" y="587.5"/> + <jdipNS:SUPPLY_CENTER x="1267" y="584"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="mun"> + <jdipNS:UNIT x="705" y="838"/> + <jdipNS:DISLODGED_UNIT x="692.5" y="825.5"/> + <jdipNS:SUPPLY_CENTER x="730" y="878"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="naf"> + <jdipNS:UNIT x="337" y="1291"/> + <jdipNS:DISLODGED_UNIT x="324.5" y="1278.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="nao"> + <jdipNS:UNIT x="191.6" y="298.2"/> + <jdipNS:DISLODGED_UNIT x="179.1" y="285.7"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="nap"> + <jdipNS:UNIT x="818" y="1180"/> + <jdipNS:DISLODGED_UNIT x="815.5" y="1167.5"/> + <jdipNS:SUPPLY_CENTER x="778" y="1143"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="nat"> + <jdipNS:UNIT x="238" y="427"/> + <jdipNS:DISLODGED_UNIT x="225.5" y="414.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="nrg"> + <jdipNS:UNIT x="605" y="250"/> + <jdipNS:DISLODGED_UNIT x="592.5" y="237.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="nth"> + <jdipNS:UNIT x="565" y="570"/> + <jdipNS:DISLODGED_UNIT x="552.5" y="557.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="nwg"> + <jdipNS:UNIT x="664.2" y="191.8"/> + <jdipNS:DISLODGED_UNIT x="651.7" y="179.3"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="nwy"> + <jdipNS:UNIT x="715" y="420"/> + <jdipNS:DISLODGED_UNIT x="702.5" y="407.5"/> + <jdipNS:SUPPLY_CENTER x="761" y="463"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="par"> + <jdipNS:UNIT x="500" y="855"/> + <jdipNS:DISLODGED_UNIT x="487.5" y="842.5"/> + <jdipNS:SUPPLY_CENTER x="518" y="819"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="pic"> + <jdipNS:UNIT x="535" y="791"/> + <jdipNS:DISLODGED_UNIT x="522.5" y="778.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="pie"> + <jdipNS:UNIT x="642" y="978"/> + <jdipNS:DISLODGED_UNIT x="629.5" y="965.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="por"> + <jdipNS:UNIT x="193" y="1023"/> + <jdipNS:DISLODGED_UNIT x="180.5" y="1010.5"/> + <jdipNS:SUPPLY_CENTER x="151" y="1060"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="pru"> + <jdipNS:UNIT x="877" y="700"/> + <jdipNS:DISLODGED_UNIT x="864.5" y="687.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="rom"> + <jdipNS:UNIT x="743" y="1112"/> + <jdipNS:DISLODGED_UNIT x="730.5" y="1099.5"/> + <jdipNS:SUPPLY_CENTER x="719" y="1087"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="ruh"> + <jdipNS:UNIT x="648" y="789"/> + <jdipNS:DISLODGED_UNIT x="635.5" y="776.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="rum"> + <jdipNS:UNIT x="1108" y="977"/> + <jdipNS:DISLODGED_UNIT x="1095.5" y="964.5"/> + <jdipNS:SUPPLY_CENTER x="1083" y="1014"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="ser"> + <jdipNS:UNIT x="945" y="1060"/> + <jdipNS:DISLODGED_UNIT x="932.5" y="1047.5"/> + <jdipNS:SUPPLY_CENTER x="936" y="1021"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="sev"> + <jdipNS:UNIT x="1296" y="855"/> + <jdipNS:DISLODGED_UNIT x="1283.5" y="842.5"/> + <jdipNS:SUPPLY_CENTER x="1267" y="973"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="sil"> + <jdipNS:UNIT x="844" y="779"/> + <jdipNS:DISLODGED_UNIT x="831.5" y="766.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="ska"> + <jdipNS:UNIT x="747" y="528"/> + <jdipNS:DISLODGED_UNIT x="734.5" y="515.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="smy"> + <jdipNS:UNIT x="1265" y="1220"/> + <jdipNS:DISLODGED_UNIT x="1252.5" y="1207.5"/> + <jdipNS:SUPPLY_CENTER x="1132" y="1222"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="spa"> + <jdipNS:UNIT x="347" y="1049"/> + <jdipNS:DISLODGED_UNIT x="334.5" y="1036.5"/> + <jdipNS:SUPPLY_CENTER x="302" y="1056"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="stp"> + <jdipNS:UNIT x="1178" y="415"/> + <jdipNS:DISLODGED_UNIT x="1165.5" y="402.5"/> + <jdipNS:SUPPLY_CENTER x="1113" y="469"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="swe"> + <jdipNS:UNIT x="841" y="469"/> + <jdipNS:DISLODGED_UNIT x="828.5" y="456.5"/> + <jdipNS:SUPPLY_CENTER x="890" y="489"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="swi"> + <jdipNS:UNIT x="642" y="928"/> + <jdipNS:DISLODGED_UNIT x="629" y="915.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="syr"> + <jdipNS:UNIT x="1464" y="1216"/> + <jdipNS:DISLODGED_UNIT x="1451.5" y="1203.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="tri"> + <jdipNS:UNIT x="837" y="1006"/> + <jdipNS:DISLODGED_UNIT x="824.5" y="993.5"/> + <jdipNS:SUPPLY_CENTER x="794" y="975"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="tun"> + <jdipNS:UNIT x="634" y="1310"/> + <jdipNS:DISLODGED_UNIT x="621.5" y="1297.5"/> + <jdipNS:SUPPLY_CENTER x="647" y="1282"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="tus"> + <jdipNS:UNIT x="698" y="1044"/> + <jdipNS:DISLODGED_UNIT x="685.5" y="1031.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="tyn"> + <jdipNS:UNIT x="720" y="1160"/> + <jdipNS:DISLODGED_UNIT x="707.5" y="1147.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="tyr"> + <jdipNS:UNIT x="754" y="914"/> + <jdipNS:DISLODGED_UNIT x="741.5" y="901.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="tys"> + <jdipNS:UNIT x="710.0" y="1159.1"/> + <jdipNS:DISLODGED_UNIT x="697.5" y="1146.6"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="ukr"> + <jdipNS:UNIT x="1136" y="810"/> + <jdipNS:DISLODGED_UNIT x="1123.5" y="797.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="ven"> + <jdipNS:UNIT x="719" y="1004"/> + <jdipNS:DISLODGED_UNIT x="706.5" y="991.5"/> + <jdipNS:SUPPLY_CENTER x="733" y="971"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="vie"> + <jdipNS:UNIT x="867" y="874"/> + <jdipNS:DISLODGED_UNIT x="854.5" y="861.5"/> + <jdipNS:SUPPLY_CENTER x="835" y="887"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="wal"> + <jdipNS:UNIT x="440" y="668"/> + <jdipNS:DISLODGED_UNIT x="427.5" y="655.5"/> + <jdipNS:SUPPLY_CENTER x="440" y="712"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="war"> + <jdipNS:UNIT x="995" y="750"/> + <jdipNS:DISLODGED_UNIT x="982.5" y="737.5"/> + <jdipNS:SUPPLY_CENTER x="937" y="748"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="wes"> + <jdipNS:UNIT x="474" y="1173"/> + <jdipNS:DISLODGED_UNIT x="461.5" y="1160.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="yor"> + <jdipNS:UNIT x="504" y="626"/> + <jdipNS:DISLODGED_UNIT x="491.5" y="613.5"/> + <jdipNS:SUPPLY_CENTER x="506" y="647"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="stp-nc"> + <jdipNS:UNIT x="1218" y="222"/> + <jdipNS:DISLODGED_UNIT x="1205.5" y="209.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="stp-sc"> + <jdipNS:UNIT x="1066" y="487"/> + <jdipNS:DISLODGED_UNIT x="1053.5" y="474.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bul-ec"> + <jdipNS:UNIT x="1127" y="1067"/> + <jdipNS:DISLODGED_UNIT x="1114.5" y="1054.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="bul-sc"> + <jdipNS:UNIT x="1070" y="1140"/> + <jdipNS:DISLODGED_UNIT x="1057.5" y="1127.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="spa-nc"> + <jdipNS:UNIT x="289" y="965"/> + <jdipNS:DISLODGED_UNIT x="276.5" y="952.5"/> + </jdipNS:PROVINCE> + <jdipNS:PROVINCE name="spa-sc"> + <jdipNS:UNIT x="291" y="1166"/> + <jdipNS:DISLODGED_UNIT x="278.5" y="1153.5"/> + </jdipNS:PROVINCE> + </jdipNS:PROVINCE_DATA> + + <defs> + <style type="text/css"><![CDATA[ + + /* text */ + svg { font-size: 100% } + .titletext {text-anchor:middle; stroke-width:0.3; font-family:sans-serif; font-size:0.7em; stroke:black; fill:black;} + .provtext {text-anchor:middle; stroke-width:0.3; font-family:sans-serif; font-size:0.7em; stroke:black; fill:black;} + .labeltext {stroke-width:0.1; stroke:black; fill:black;} + .unordered {fill:red; stroke:black; stroke-width:1; fill-opacity:0.90;} + .labeltext24 {text-anchor:middle; stroke-width:0.1; stroke:black; fill:black; font-family:serif,sans-serif; font-style:italic; font-size:1.4em;} + .labeltext18 {text-anchor:middle; stroke-width:0.1; stroke:black; fill:black; font-family:serif,sans-serif; font-style:italic; font-size:1.1em;} + .fulllabeltext {font-family:serif,sans-serif; font-style:italic; font-size:1.2em; fill:black; stroke:black;} + .currentnotetext {font-family:serif,sans-serif; font-size:1.5em; fill:black; stroke:black;} + .currentnoterect {fill:#c5dfea;} + .currentphasetext {font-family:serif,sans-serif; font-size:2.5em; fill:black; stroke:black;} + + .labeltext24 text {cursor:default;} + + /* NB: this style is not yet used. */ + .labeltext24 text.allowed { + font-weight:bold; + fill:blue; + font-size: 1.5em; + stroke: red; + stroke-width:2; + text-decoration: underline; + background-color: green; + } + + /* map and object features */ + + .seapoly {stroke:#000000; stroke-width:1; fill:#B5DEF8} + .dashline {stroke:darkslateblue; stroke-width:3; stroke-linecap:round; stroke-dasharray:5,6;} + .impassable {fill:#353433; stroke:#000000; stroke-width:1} + .sealine {stroke:#B5DEFF; stroke-width:3;} + + /* invisible click rects fill:none does not work */ + + .invisible {stroke:#000000; fill:#000000; fill-opacity:0.0; opacity:0.0} + + /* default region coloring, by power */ + + .provinceRed {fill:url(#patternRed)} + .provinceBrown {fill:url(#patternBrown)} + .provinceGreen {fill:url(#patternGreen)} + .provinceBlack {fill:url(#patternBlack)} + .provinceBlue {fill:url(#patternBlue)} + + .nopower {fill:antiquewhite; stroke:#000000; stroke-width:1} + .water {fill:#c5dfea; stroke:#000000; stroke-width:1} + + .neutral {fill:lightgray; stroke:#000000; stroke-width:1} + + .austria {fill:#c48f85; stroke:#000000; stroke-width:1} + .england {fill:darkviolet; stroke:#000000; stroke-width:1} + .france {fill:royalblue; stroke:#000000; stroke-width:1} + .germany {fill:#a08a75; stroke:#000000; stroke-width:1} + .italy {fill:forestgreen; stroke:#000000; stroke-width:1} + .russia {fill:#757d91; stroke:#000000; stroke-width:1} + .turkey {fill:#b9a61c; stroke:#000000; stroke-width:1} + + /* unit colors, by power note that underscores are not supported */ + + .unitaustria {fill:red; fill-opacity:0.85} + .unitengland {fill:mediumpurple; fill-opacity:0.85} + .unitfrance {fill:deepskyblue; fill-opacity:0.85} + .unitgermany {fill:dimgray; fill-opacity:0.85} + .unititaly {fill:olive; fill-opacity:0.85} + .unitrussia {fill:white; fill-opacity:1.0} + .unitturkey {fill:yellow; fill-opacity:0.85} + + /* supply center styles */ + + .scnopower {fill:black; stroke:black;} + .scaustria {fill:black; stroke:black;} + .scengland {fill:black; stroke:black;} + .scfrance {fill:black; stroke:black;} + .scgermany {fill:black; stroke:black;} + .scitaly {fill:black; stroke:black;} + .scrussia {fill:black; stroke:black;} + .scturkey {fill:black; stroke:black;} + + /* order drawing styles, stroke and fill colors should not be specified */ + + .defaultorder {stroke-width:6; fill:none;} + .supportorder {stroke-width:6; fill:none; stroke-dasharray:5,5;} + .convoyorder {stroke-dasharray:15,5; stroke-width:6; fill:none;} + + .shadoworder {stroke-width:10; fill:none; stroke:black;} + .shadowdash {stroke-width:10; fill:none; stroke:black; opacity:0.45;} + + .varwidthorder {fill:none;} + .varwidthshadow {fill:none; stroke:black;} + + /* Symbol private styles. Always start with "sym" to avoid name collisions! */ + + .symBuildShadow {fill:none;stroke:black;opacity:0.5;stroke-width:7;} + .symBuild {stroke:yellow;stroke-width:7;fill:none;} + .symRemove {stroke:red;stroke-width:1;fill:none;} + + .symShadow {stroke:black;fill:black;stroke-width:1;opacity:0.40;} + .symDislodgedShadow {stroke:red;fill:red;stroke-width:1;opacity:0.50;} + .symDislodgedBorder {stroke:red;stroke-width:3%;} + + .symDarkener {fill:black;opacity:0.45;fill-opacity:0.45;} + .symCenterHub {fill:black; stroke:black; opacity:0.60; stroke-width:0.5px;} + .symBorder {stroke:black;stroke-width:3%;} + .symThinBorder {stroke:black;stroke-width:0.4;} + .symSilhouette {stroke:black;fill:black;stroke-width:1;} + + ]]></style> + + <!-- NON UNIT SYMBOLS --> + <symbol id="WaivedBuild" viewBox="0 0 100 100" overflow="visible"> + <linearGradient x1="15" y1="100" x2="100" y2="10" id="symWBGradient" gradientUnits="userSpaceOnUse"> + <stop offset="20%" stop-color="yellow" stop-opacity="1"/> + <stop offset="95%" stop-color="yellow" stop-opacity="0"/> + </linearGradient> + <linearGradient x1="15" y1="100" x2="100" y2="10" id="symShadowWBGradient" gradientUnits="userSpaceOnUse"> + <stop offset="20%" stop-color="black" stop-opacity="0.5"/> + <stop offset="90%" stop-color="black" stop-opacity="0"/> + </linearGradient> + <g> + <polygon transform="translate(1 7)" fill="url(#symShadowWBGradient)" points="40,100 100,35 95,30 40,85 13,65 10,70"/> + <polygon stroke="black" stroke-width="0.5" fill="url(#symWBGradient)" points="40,100 100,35 90,20 40,85 13,65 10,70"/> + </g> + </symbol> + + <symbol id="BuildUnit" viewBox="0 0 100 100" overflow="visible"> + <g> + <g transform="translate(6 6)" class="symBuildShadow"> + <circle cx="50" cy="50" r="10"/> + <circle cx="50" cy="50" r="30"/> + <circle cx="50" cy="50" r="50"/> + <circle cx="50" cy="50" r="70"/> + </g> + <g class="symBuild"> + <circle cx="50" cy="50" r="10"/> + <circle cx="50" cy="50" r="30"/> + <circle cx="50" cy="50" r="50"/> + <circle cx="50" cy="50" r="70"/> + </g> + </g> + </symbol> + + <symbol id="RemoveUnit" viewBox="0 0 10 10" overflow="visible"> + <g class="symRemove"> + <circle cx="5" cy="5" r="7"/> + <line x1="-2" y1="-2" x2="12" y2="12"/> + <line x1="-2" y1="12" x2="12" y2="-2"/> + </g> + </symbol> + + <symbol id="FailedOrder" viewBox="0 0 35 35" overflow="visible"> + <g> + <polygon transform="translate(3.5,3.5)" class="shadow" stroke-width="1" points="0,0 12,0 17,6 22,0 35,0 22,17 32,34 19,34 15,27 9,34 -4,34 10,17"/> + <polygon stroke="black" fill="red" stroke-width="3%" fill-opacity="1" points="0,0 12,0 17,6 22,0 35,0 22,17 32,34 19,34 15,27 9,34 -4,34 10,17"/> + </g> + </symbol> + + <symbol id="SupplyCenter" viewBox="0 0 10 10" overflow="visible"> + <g> + <circle cx="5" cy="5" r="3" class="symThinBorder"/> + <circle cx="5" cy="5" r="5" stroke-width="0.75" stroke="black" fill="none"/> + </g> + </symbol> + + <!-- UNIT SYMBOLS --> + <symbol id="Army" viewBox="0 0 23 15" overflow="visible"> + <g> + <rect x="2" y="2" width="23" height="13" rx="4" stroke-width="1" class="symShadow" /> + <rect x="0" y="0" width="23" height="13" rx="4" class="symBorder" /> + <g class="symSilhouette"> + <rect x="6" y="6" width="13" height="1"/> + <rect x="5" y="7" width="14" height="1"/> + <rect x="6" y="8" width="12" height="1"/> + <rect x="7" y="9" width="10" height="1"/> + <rect x="10" y="3" width="5" height="3"/> + <rect x="15" y="4.5" width="1" height="1.5"/> + <line x1="3" y1="4" x2="10" y2="4"/> + </g> + </g> + </symbol> + + <symbol id="Fleet" viewBox="0 0 23 15" overflow="visible"> + <g> + <rect x="2" y="2" width="23" height="13" rx="4" stroke-width="1" class="symShadow" /> + <rect x="0" y="0" width="23" height="13" rx="4" class="symBorder" /> + <g class="symSilhouette"> + <rect x="3" y="7" width="16.5" height="1"/> + <rect x="4" y="8" width="15" height="1"/> + <rect x="5" y="9" width="13.5" height="1"/> + <rect x="13.5" y="6" width="2.75" height="1"/> + <rect x="7" y="5" width="4" height="2"/> + <rect x="8.5" y="4" width="1" height="1"/> + <rect x="6" y="6" width="1" height="1"/> + </g> + </g> + </symbol> + + <!-- DISLODGED UNIT SYMBOLS --> + <symbol id="DislodgedArmy" viewBox="0 0 23 15" overflow="visible"> + <g> + <rect x="3" y="3" width="23" height="13" rx="4" stroke-width="1" class="symDislodgedShadow" /> + <rect x="0" y="0" width="23" height="13" rx="4" class="symDislodgedBorder" /> + <g class="symSilhouette"> + <rect x="6" y="6" width="13" height="1"/> + <rect x="5" y="7" width="14" height="1"/> + <rect x="6" y="8" width="12" height="1"/> + <rect x="7" y="9" width="10" height="1"/> + <rect x="10" y="3" width="5" height="3"/> + <rect x="15" y="4.5" width="1" height="1.5"/> + <line x1="3" y1="4" x2="10" y2="4"/> + </g> + </g> + </symbol> + + <symbol id="DislodgedFleet" viewBox="0 0 23 15" overflow="visible"> + <g> + <rect x="3" y="3" width="23" height="13" rx="4" stroke-width="1" class="symDislodgedShadow" /> + <rect x="0" y="0" width="23" height="13" rx="4" class="symDislodgedBorder" /> + <g class="symSilhouette"> + <rect x="3" y="7" width="16.5" height="1"/> + <rect x="4" y="8" width="15" height="1"/> + <rect x="5" y="9" width="13.5" height="1"/> + <rect x="13.5" y="6" width="2.75" height="1"/> + <rect x="7" y="5" width="4" height="2"/> + <rect x="8.5" y="4" width="1" height="1"/> + <rect x="6" y="6" width="1" height="1"/> + </g> + </g> + </symbol> + + <marker id="arrow" markerHeight="4" markerUnits="strokeWidth" markerWidth="4" orient="auto" refX="5" refY="5" viewBox="0 0 10 10"> + <path d="M 0 0 L 10 5 L 0 10 z"/> + </marker> + + <pattern id="patternRed" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse"> + <rect x="0" y="0" width="10" height="10" fill="red"/> + <rect x="5" y="0" width="10" height="10" fill="pink"/> + </pattern> + <pattern id="patternBrown" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse"> + <rect x="0" y="0" width="10" height="10" fill="peru"/> + <rect x="5" y="0" width="10" height="10" fill="antiquewhite"/> + </pattern> + <pattern id="patternGreen" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse"> + <rect x="0" y="0" width="10" height="10" fill="seagreen"/> + <rect x="5" y="0" width="10" height="10" fill="yellowgreen"/> + </pattern> + <pattern id="patternBlue" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse"> + <rect x="0" y="0" width="10" height="10" fill="CornflowerBlue"/> + <rect x="5" y="0" width="10" height="10" fill="cyan"/> + </pattern> + <pattern id="patternBlack" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse"> + <rect x="0" y="0" width="10" height="10" fill="black"/> + <rect x="0" y="5" width="10" height="10" fill="gray"/> + </pattern> + </defs> + + <g id="MapLayer" transform="translate(-195 -170)"> + <rect fill="black" height="1360" width="1835" x="195" y="170"/> + <path class="nopower" d="M 1424 1364 C 1437 1361 1448 1353 1459 1346 C 1464 1343 1470 1337 1475 1336 C 1482 1334 1492 1338 1499 1340 C 1510 1342 1518 1341 1528 1336 C 1544 1328 1555 1307 1575 1297 C 1587 1291 1598 1293 1611 1293 C 1614 1293 1621 1292 1624 1292 C 1646 1286 1638 1257 1637 1241 C 1618 1244 1604 1253 1583 1253 C 1566 1253 1565 1248 1554 1246 C 1553 1247 1553 1248 1551 1248 C 1548 1249 1541 1242 1538 1240 C 1535 1242 1529 1247 1526 1246 C 1521 1245 1517 1235 1511 1235 C 1507 1236 1507 1239 1497 1241 C 1483 1243 1471 1243 1457 1249 C 1450 1253 1440 1261 1435 1266 C 1433 1268 1421 1282 1420 1284 C 1419 1286 1419 1290 1419 1292 C 1419 1300 1423 1305 1425 1312 C 1428 1318 1430 1326 1431 1333 C 1432 1342 1427 1355 1424 1364 z" id="_ank"/> + <path class="nopower" d="M 1671 1218 C 1663 1226 1648 1237 1638 1240 C 1638 1240 1641 1241 1641 1241 C 1641 1241 1643 1272 1643 1272 C 1642 1279 1638 1298 1639 1303 C 1641 1311 1648 1312 1652 1318 C 1658 1325 1657 1329 1657 1337 C 1657 1337 1708 1337 1708 1337 C 1708 1337 1720 1338 1720 1338 C 1720 1338 1730 1338 1730 1338 C 1730 1338 1825 1348 1825 1348 C 1825 1348 1837 1349 1837 1349 C 1837 1349 1845 1349 1845 1349 C 1853 1349 1867 1347 1874 1345 C 1892 1339 1913 1320 1927 1307 C 1927 1307 1956 1280 1956 1280 C 1956 1280 1938 1277 1938 1277 C 1922 1273 1905 1266 1896 1251 C 1894 1251 1890 1252 1888 1252 C 1883 1251 1872 1247 1869 1242 C 1868 1240 1867 1220 1858 1221 C 1853 1221 1846 1232 1843 1236 C 1833 1248 1822 1262 1805 1261 C 1784 1260 1786 1252 1767 1252 C 1767 1252 1753 1252 1753 1252 C 1753 1252 1736 1251 1736 1251 C 1727 1251 1722 1253 1713 1250 C 1697 1243 1680 1223 1671 1218 z" id="_arm"/> + <path class="nopower" d="M 1331 1267 C 1320 1272 1310 1264 1298 1274 C 1289 1282 1298 1291 1291 1305 C 1289 1310 1284 1314 1287 1316 C 1289 1319 1295 1316 1303 1317 C 1303 1317 1303 1319 1303 1319 C 1299 1321 1292 1327 1290 1332 C 1294 1330 1296 1329 1297 1324 C 1297 1324 1323 1304 1323 1304 C 1323 1304 1337 1298 1337 1298 C 1337 1298 1356 1294 1356 1294 C 1356 1294 1340 1282 1340 1282 C 1340 1282 1331 1267 1331 1267 z M 1414 1284 C 1414 1284 1389 1288 1389 1288 C 1389 1288 1375 1292 1375 1292 C 1372 1292 1361 1293 1364 1299 C 1366 1301 1376 1303 1380 1304 C 1380 1304 1380 1306 1380 1306 C 1380 1306 1367 1310 1367 1310 C 1367 1310 1357 1315 1357 1315 C 1357 1315 1348 1318 1348 1318 C 1341 1321 1342 1326 1329 1326 C 1322 1326 1314 1323 1308 1328 C 1304 1331 1290 1344 1288 1349 C 1287 1351 1287 1353 1288 1356 C 1296 1354 1305 1345 1307 1359 C 1307 1359 1307 1364 1307 1364 C 1311 1361 1315 1355 1319 1355 C 1323 1354 1328 1358 1331 1360 C 1335 1363 1340 1364 1345 1365 C 1355 1366 1363 1362 1368 1361 C 1374 1361 1378 1364 1383 1365 C 1389 1365 1393 1361 1400 1361 C 1405 1361 1407 1361 1412 1362 C 1414 1363 1418 1364 1419 1363 C 1422 1362 1424 1355 1424 1352 C 1429 1337 1427 1328 1422 1314 C 1422 1314 1416 1297 1416 1297 C 1415 1292 1416 1288 1414 1284 z M 1346 1295 C 1346 1295 1346 1296 1346 1296 C 1346 1296 1345 1295 1345 1295 C 1345 1295 1346 1295 1346 1295 z" id="_con"/> + <path class="nopower" d="M 2022 319 C 2022 319 1994 345 1994 345 C 1994 345 1970 368 1970 368 C 1929 405 1867 454 1820 484 C 1791 502 1771 510 1741 525 C 1741 525 1716 539 1716 539 C 1708 544 1703 547 1694 551 C 1679 557 1664 558 1648 562 C 1618 570 1610 579 1590 585 C 1590 585 1546 593 1546 593 C 1535 596 1530 601 1521 607 C 1512 612 1496 620 1487 623 C 1468 629 1453 625 1434 632 C 1418 639 1408 654 1405 671 C 1404 678 1406 690 1401 696 C 1391 707 1376 685 1365 691 C 1360 693 1356 701 1353 706 C 1348 713 1340 724 1334 730 C 1321 742 1301 741 1284 741 C 1284 754 1279 767 1278 772 C 1278 777 1279 780 1280 784 C 1280 792 1276 793 1276 804 C 1276 804 1278 828 1278 828 C 1278 838 1278 846 1268 851 C 1264 853 1259 854 1255 856 C 1255 856 1242 870 1242 870 C 1242 870 1228 882 1228 882 C 1226 883 1222 886 1221 888 C 1220 891 1222 897 1223 900 C 1225 911 1226 909 1229 918 C 1229 918 1234 935 1234 935 C 1237 932 1238 930 1242 927 C 1247 925 1271 918 1278 916 C 1278 916 1353 897 1353 897 C 1364 894 1385 889 1396 889 C 1401 889 1407 889 1412 890 C 1415 890 1424 892 1427 892 C 1432 891 1438 885 1448 884 C 1448 884 1481 888 1481 888 C 1490 888 1497 888 1502 880 C 1506 873 1508 855 1519 855 C 1525 855 1535 862 1541 865 C 1551 869 1556 870 1567 870 C 1579 870 1580 865 1588 866 C 1593 867 1602 869 1606 872 C 1617 879 1623 900 1636 913 C 1647 924 1652 919 1660 927 C 1667 936 1662 948 1673 958 C 1678 962 1688 966 1694 968 C 1707 974 1719 977 1732 984 C 1748 993 1759 1009 1770 1017 C 1771 1015 1771 1011 1773 1010 C 1776 1008 1778 1010 1780 1010 C 1784 1009 1786 1005 1788 1002 C 1791 996 1789 991 1793 985 C 1797 977 1808 967 1815 960 C 1815 960 1828 941 1828 941 C 1834 935 1842 931 1849 928 C 1863 923 1881 917 1889 934 C 1892 941 1895 955 1895 963 C 1895 969 1896 975 1892 980 C 1887 988 1878 987 1874 997 C 1869 1008 1879 1013 1874 1020 C 1869 1028 1858 1029 1854 1040 C 1866 1041 1876 1045 1885 1053 C 1892 1060 1891 1065 1895 1067 C 1897 1067 1900 1067 1902 1067 C 1907 1067 1909 1070 1916 1068 C 1920 1068 1926 1064 1930 1065 C 1936 1066 1936 1072 1936 1077 C 1936 1088 1935 1099 1947 1105 C 1947 1096 1946 1088 1954 1081 C 1957 1079 1960 1079 1964 1080 C 1969 1081 1975 1087 1980 1090 C 1985 1093 1994 1094 1997 1100 C 1999 1105 1996 1111 1994 1115 C 1987 1126 1985 1124 1975 1127 C 1973 1128 1969 1130 1967 1130 C 1965 1130 1961 1128 1959 1127 C 1959 1138 1964 1147 1970 1156 C 1976 1153 1983 1143 1989 1143 C 1991 1142 1993 1143 1993 1145 C 1993 1148 1990 1151 1990 1154 C 1989 1160 1993 1158 1988 1166 C 1988 1166 2007 1175 2007 1175 C 2007 1175 2013 1183 2013 1183 C 2013 1183 2022 1217 2022 1217 C 2022 1217 2023 1195 2023 1195 C 2023 1195 2023 1149 2023 1149 C 2023 1149 2023 990 2023 990 C 2023 990 2023 516 2023 516 C 2023 516 2023 378 2023 378 C 2023 378 2023 338 2023 338 C 2023 338 2022 319 2022 319 z" id="_mos"/> + <path class="nopower" d="M 1364 1100 C 1367 1100 1372 1102 1374 1101 C 1376 1101 1381 1094 1383 1092 C 1389 1087 1394 1085 1401 1084 C 1404 1083 1410 1082 1413 1084 C 1419 1086 1415 1090 1411 1091 C 1411 1091 1406 1092 1406 1092 C 1408 1094 1410 1099 1412 1100 C 1422 1108 1442 1099 1446 1098 C 1450 1098 1453 1101 1452 1105 C 1452 1110 1446 1112 1442 1115 C 1437 1118 1435 1122 1434 1128 C 1439 1129 1450 1131 1453 1135 C 1457 1140 1451 1155 1464 1157 C 1466 1157 1467 1157 1469 1156 C 1474 1154 1480 1144 1485 1140 C 1491 1136 1499 1137 1499 1127 C 1503 1126 1504 1126 1508 1126 C 1508 1126 1512 1126 1512 1126 C 1514 1126 1518 1126 1519 1124 C 1521 1122 1520 1115 1516 1113 C 1509 1111 1504 1119 1499 1120 C 1490 1122 1478 1109 1471 1104 C 1469 1103 1461 1101 1461 1098 C 1460 1095 1464 1094 1466 1093 C 1474 1090 1479 1088 1486 1083 C 1486 1083 1512 1061 1512 1061 C 1512 1061 1549 1043 1549 1043 C 1549 1043 1564 1033 1564 1033 C 1566 1032 1569 1030 1571 1031 C 1575 1033 1571 1039 1569 1041 C 1569 1041 1543 1061 1543 1061 C 1545 1064 1546 1066 1549 1067 C 1552 1067 1556 1066 1557 1070 C 1558 1073 1557 1077 1556 1080 C 1556 1080 1549 1097 1549 1097 C 1548 1102 1549 1109 1544 1112 C 1540 1115 1536 1112 1530 1111 C 1531 1119 1535 1119 1542 1122 C 1542 1122 1554 1126 1554 1126 C 1554 1126 1571 1130 1571 1130 C 1584 1134 1594 1138 1605 1145 C 1613 1150 1620 1157 1628 1160 C 1640 1166 1653 1164 1664 1171 C 1672 1176 1682 1188 1681 1198 C 1680 1204 1676 1209 1677 1212 C 1678 1216 1690 1226 1693 1228 C 1699 1234 1710 1242 1718 1245 C 1724 1246 1727 1244 1734 1244 C 1734 1244 1749 1246 1749 1246 C 1756 1246 1759 1245 1765 1245 C 1782 1244 1789 1254 1804 1255 C 1820 1256 1831 1242 1840 1230 C 1844 1224 1849 1215 1857 1214 C 1866 1213 1872 1224 1873 1231 C 1874 1233 1874 1237 1875 1239 C 1878 1244 1887 1246 1892 1245 C 1890 1239 1886 1229 1886 1223 C 1886 1218 1889 1218 1889 1212 C 1889 1203 1883 1184 1891 1178 C 1895 1175 1899 1174 1903 1173 C 1900 1167 1894 1170 1888 1169 C 1883 1168 1880 1166 1876 1163 C 1876 1163 1855 1144 1855 1144 C 1847 1139 1843 1139 1839 1136 C 1839 1136 1831 1128 1831 1128 C 1824 1122 1818 1118 1811 1110 C 1799 1097 1803 1092 1796 1084 C 1793 1079 1788 1075 1782 1073 C 1778 1071 1774 1071 1772 1068 C 1769 1064 1768 1055 1768 1051 C 1768 1051 1769 1035 1769 1035 C 1769 1027 1769 1028 1770 1020 C 1765 1019 1762 1014 1759 1011 C 1759 1011 1738 992 1738 992 C 1724 982 1708 978 1692 971 C 1685 968 1674 964 1668 958 C 1660 948 1662 936 1658 930 C 1655 926 1651 926 1646 924 C 1642 922 1639 920 1636 917 C 1628 909 1623 901 1617 892 C 1614 886 1609 877 1603 874 C 1599 871 1587 869 1582 870 C 1578 871 1576 873 1569 874 C 1559 875 1550 873 1541 868 C 1535 866 1522 857 1516 860 C 1513 861 1512 865 1510 868 C 1507 875 1506 882 1500 888 C 1488 897 1465 889 1451 888 C 1446 888 1433 891 1431 896 C 1430 898 1431 902 1432 904 C 1432 908 1434 917 1434 921 C 1434 921 1429 938 1429 938 C 1426 958 1424 989 1411 1006 C 1404 1015 1391 1024 1381 1029 C 1381 1029 1367 1035 1367 1035 C 1363 1038 1362 1041 1359 1045 C 1359 1045 1350 1058 1350 1058 C 1348 1063 1348 1065 1344 1070 C 1340 1074 1335 1079 1330 1083 C 1328 1085 1323 1088 1322 1090 C 1321 1092 1324 1103 1325 1107 C 1325 1115 1320 1130 1329 1140 C 1334 1144 1339 1142 1344 1141 C 1347 1140 1356 1139 1357 1137 C 1359 1136 1360 1132 1361 1130 C 1362 1126 1368 1116 1369 1113 C 1369 1108 1365 1107 1364 1100 z" id="_sev"/> + <path class="nopower" d="M 1586 175 C 1590 180 1597 184 1598 190 C 1599 194 1597 196 1597 201 C 1598 204 1601 211 1595 211 C 1590 211 1588 204 1581 201 C 1578 207 1576 215 1572 220 C 1566 226 1562 224 1556 230 C 1551 236 1552 242 1550 245 C 1549 248 1547 248 1546 251 C 1544 254 1543 262 1543 266 C 1541 271 1539 277 1539 282 C 1540 291 1546 299 1547 308 C 1542 306 1541 303 1538 299 C 1530 288 1528 274 1530 261 C 1532 255 1537 239 1528 236 C 1525 235 1522 237 1522 240 C 1520 245 1523 248 1521 258 C 1520 257 1518 255 1516 255 C 1513 255 1511 258 1510 260 C 1506 265 1503 270 1500 276 C 1495 286 1493 293 1491 304 C 1490 311 1493 316 1489 322 C 1483 334 1471 334 1460 329 C 1456 328 1452 326 1449 323 C 1441 312 1463 307 1455 295 C 1450 288 1442 289 1434 288 C 1429 287 1424 286 1419 289 C 1428 296 1432 302 1434 313 C 1435 318 1436 327 1438 331 C 1440 335 1443 334 1446 337 C 1449 339 1451 342 1452 346 C 1453 351 1454 365 1449 368 C 1447 369 1445 369 1443 369 C 1438 369 1431 368 1426 372 C 1423 374 1418 386 1413 392 C 1410 396 1402 404 1403 409 C 1404 413 1409 416 1412 419 C 1416 422 1418 424 1421 428 C 1431 438 1435 433 1443 436 C 1445 437 1450 439 1448 442 C 1446 446 1437 443 1434 442 C 1426 441 1420 441 1412 441 C 1403 441 1398 443 1388 440 C 1381 438 1374 430 1369 433 C 1366 435 1366 440 1367 443 C 1368 448 1372 454 1377 455 C 1382 457 1385 454 1391 460 C 1396 466 1398 468 1392 474 C 1391 475 1390 477 1388 477 C 1384 479 1372 473 1368 471 C 1361 468 1352 467 1346 461 C 1343 457 1341 452 1338 448 C 1336 444 1331 439 1330 434 C 1328 428 1331 419 1326 414 C 1324 411 1319 411 1314 408 C 1314 408 1303 400 1303 400 C 1297 395 1296 396 1289 389 C 1288 387 1283 382 1286 380 C 1289 376 1295 385 1302 386 C 1302 386 1313 386 1313 386 C 1313 386 1322 390 1322 390 C 1329 391 1351 394 1358 394 C 1365 394 1378 395 1384 392 C 1395 387 1409 365 1406 352 C 1405 349 1394 336 1392 334 C 1383 327 1369 324 1358 321 C 1358 321 1313 304 1313 304 C 1303 303 1291 306 1284 306 C 1281 306 1272 304 1272 300 C 1272 297 1275 298 1276 291 C 1272 290 1267 289 1263 292 C 1257 296 1255 305 1252 312 C 1250 316 1246 322 1243 326 C 1241 329 1238 333 1237 336 C 1236 339 1237 344 1237 347 C 1240 361 1249 363 1251 372 C 1251 372 1253 396 1253 396 C 1254 406 1256 416 1258 426 C 1262 439 1275 471 1280 483 C 1287 498 1297 513 1297 530 C 1297 540 1292 539 1292 553 C 1292 553 1293 561 1293 561 C 1293 574 1286 587 1279 598 C 1276 603 1269 613 1272 619 C 1276 624 1284 623 1289 625 C 1293 626 1297 629 1300 631 C 1292 639 1293 635 1285 636 C 1285 636 1273 641 1273 641 C 1268 643 1266 651 1262 655 C 1258 658 1254 658 1249 658 C 1249 658 1226 656 1226 656 C 1214 656 1205 659 1194 665 C 1200 677 1215 678 1227 678 C 1232 678 1239 677 1243 678 C 1255 680 1256 690 1262 698 C 1267 705 1272 707 1276 712 C 1282 718 1284 729 1284 737 C 1301 737 1320 739 1334 726 C 1343 717 1349 705 1356 695 C 1360 690 1364 685 1371 686 C 1377 687 1383 692 1390 694 C 1393 695 1396 695 1398 693 C 1401 691 1401 683 1401 680 C 1401 673 1402 667 1404 661 C 1414 632 1438 623 1466 623 C 1484 623 1504 613 1519 604 C 1531 597 1535 592 1550 588 C 1550 588 1590 581 1590 581 C 1590 581 1643 560 1643 560 C 1643 560 1693 548 1693 548 C 1702 544 1709 539 1717 534 C 1717 534 1736 524 1736 524 C 1736 524 1795 495 1795 495 C 1825 477 1854 457 1882 436 C 1909 416 1934 395 1959 373 C 1959 373 1978 356 1978 356 C 1978 356 2008 328 2008 328 C 2012 324 2017 317 2023 316 C 2023 316 2023 175 2023 175 C 2023 175 1586 175 1586 175 z" id="_stp"/> + <path class="nopower" d="M 2022 1247 C 2020 1256 2012 1263 2005 1268 C 1988 1280 1980 1283 1959 1280 C 1958 1285 1953 1287 1950 1290 C 1950 1290 1934 1305 1934 1305 C 1918 1320 1892 1344 1871 1350 C 1862 1352 1859 1351 1850 1352 C 1850 1352 1842 1353 1842 1353 C 1842 1353 1831 1352 1831 1352 C 1798 1352 1766 1345 1733 1342 C 1709 1340 1683 1341 1659 1341 C 1645 1341 1636 1348 1624 1353 C 1614 1358 1609 1356 1600 1365 C 1591 1374 1592 1381 1588 1392 C 1582 1405 1577 1410 1576 1412 C 1576 1412 1574 1420 1574 1420 C 1574 1420 1568 1433 1568 1433 C 1568 1433 1572 1437 1572 1437 C 1572 1437 1573 1459 1573 1459 C 1580 1460 1579 1463 1579 1468 C 1579 1468 1581 1475 1581 1475 C 1583 1484 1582 1484 1582 1492 C 1582 1492 1584 1509 1584 1509 C 1584 1509 1584 1527 1584 1527 C 1584 1527 2023 1527 2023 1527 C 2023 1527 2023 1330 2023 1330 C 2023 1330 2023 1273 2023 1273 C 2023 1273 2022 1247 2022 1247 z" id="_syr"/> + <path class="nopower" d="M 1275 1047 C 1293 1047 1303 1065 1311 1078 C 1311 1078 1318 1088 1318 1088 C 1324 1084 1337 1074 1341 1068 C 1344 1064 1344 1062 1347 1058 C 1347 1058 1362 1035 1362 1035 C 1367 1030 1371 1029 1377 1027 C 1383 1024 1390 1020 1395 1017 C 1415 1003 1417 987 1421 965 C 1421 965 1426 935 1426 935 C 1428 924 1431 925 1429 912 C 1429 909 1427 899 1425 897 C 1424 895 1419 895 1417 894 C 1409 893 1402 893 1394 893 C 1383 893 1365 898 1354 901 C 1354 901 1275 921 1275 921 C 1275 921 1257 926 1257 926 C 1249 928 1241 929 1237 938 C 1234 947 1237 953 1233 965 C 1232 968 1229 973 1230 976 C 1233 983 1249 989 1256 995 C 1266 1005 1275 1025 1275 1039 C 1275 1039 1275 1047 1275 1047 z" id="_ukr"/> + <path class="nopower" d="M 1190 668 C 1187 675 1186 680 1189 688 C 1192 699 1199 697 1203 702 C 1206 705 1206 708 1206 711 C 1207 719 1207 731 1204 738 C 1202 742 1197 749 1192 747 C 1185 745 1175 723 1164 725 C 1159 726 1157 731 1155 735 C 1151 744 1145 758 1145 767 C 1145 770 1145 777 1146 780 C 1148 782 1152 782 1157 789 C 1159 794 1160 800 1165 803 C 1171 808 1178 805 1185 815 C 1191 823 1191 835 1188 844 C 1187 848 1184 853 1185 857 C 1188 866 1202 873 1209 879 C 1212 881 1214 883 1217 886 C 1225 881 1236 871 1243 864 C 1243 864 1251 855 1251 855 C 1257 849 1263 850 1268 848 C 1275 843 1274 833 1274 826 C 1274 817 1271 810 1272 801 C 1273 796 1276 793 1276 786 C 1276 780 1273 778 1274 771 C 1274 771 1278 757 1278 757 C 1280 747 1281 732 1278 722 C 1274 710 1266 710 1260 701 C 1255 695 1253 686 1246 682 C 1240 679 1231 682 1224 682 C 1218 682 1207 680 1202 677 C 1198 675 1194 671 1190 668 z" id="_lvn"/> + <path class="nopower" d="M 1180 860 C 1162 885 1123 876 1100 892 C 1073 911 1079 940 1096 963 C 1098 966 1107 976 1109 978 C 1113 981 1125 983 1130 983 C 1138 983 1146 978 1152 973 C 1155 970 1157 966 1160 965 C 1167 962 1176 971 1186 973 C 1200 977 1205 969 1213 967 C 1218 967 1222 970 1226 972 C 1230 962 1231 958 1232 947 C 1232 942 1228 923 1226 918 C 1226 918 1221 908 1221 908 C 1219 901 1219 894 1214 888 C 1209 881 1201 878 1194 873 C 1189 869 1186 863 1180 860 z" id="_war"/> + <path class="nopower" d="M 1146 788 C 1147 796 1148 812 1141 817 C 1137 821 1131 817 1127 821 C 1125 824 1125 827 1123 830 C 1122 833 1119 836 1116 838 C 1109 843 1100 847 1093 840 C 1090 837 1090 835 1090 831 C 1087 831 1083 830 1080 830 C 1069 831 1058 838 1048 842 C 1031 848 1028 850 1010 853 C 1010 853 1014 883 1014 883 C 1016 891 1018 893 1019 898 C 1021 904 1019 909 1022 912 C 1025 915 1030 916 1033 916 C 1033 916 1061 918 1061 918 C 1064 918 1072 919 1074 917 C 1074 917 1081 904 1081 904 C 1085 896 1092 890 1099 885 C 1111 878 1130 875 1144 872 C 1154 870 1164 868 1172 861 C 1176 857 1179 851 1181 846 C 1182 841 1183 839 1183 834 C 1183 834 1183 830 1183 830 C 1183 824 1182 818 1177 815 C 1172 812 1168 814 1161 808 C 1151 799 1155 792 1146 788 z" id="_pru"/> + <path class="nopower" d="M 960 935 C 960 935 970 964 970 964 C 970 964 997 957 997 957 C 1004 956 1012 954 1019 956 C 1029 960 1036 969 1044 975 C 1053 982 1055 981 1065 984 C 1080 989 1090 995 1104 983 C 1104 983 1088 962 1088 962 C 1079 949 1075 937 1075 921 C 1075 921 1039 921 1039 921 C 1039 921 1027 919 1027 919 C 1027 919 1019 916 1019 916 C 1019 916 1012 921 1012 921 C 1012 921 1002 926 1002 926 C 1002 926 960 935 960 935 z" id="_sil"/> + <path class="nopower" d="M 939 938 C 939 938 969 929 969 929 C 969 929 1001 923 1001 923 C 1001 923 1017 913 1017 913 C 1017 913 1016 899 1016 899 C 1016 899 1010 884 1010 884 C 1008 875 1007 862 1007 853 C 1001 852 995 851 990 847 C 988 846 985 843 986 840 C 987 838 988 837 989 836 C 988 835 987 834 985 833 C 980 832 966 839 959 841 C 955 842 949 842 947 845 C 947 845 943 868 943 868 C 941 880 936 900 936 912 C 936 920 936 931 939 938 z" id="_ber"/> + <path class="nopower" d="M 823 916 C 842 925 854 923 872 938 C 878 944 883 947 888 954 C 890 956 892 960 895 960 C 899 961 903 956 906 954 C 913 949 918 950 927 945 C 929 943 933 941 933 939 C 935 937 934 934 933 932 C 933 932 932 918 932 918 C 932 913 932 912 933 908 C 933 908 936 886 936 886 C 938 874 942 860 942 848 C 942 848 926 848 926 848 C 928 845 933 839 933 836 C 932 832 926 829 923 825 C 918 818 915 812 915 803 C 915 803 900 803 900 803 C 898 803 894 803 893 804 C 889 807 893 819 893 823 C 893 826 891 839 890 842 C 889 844 888 846 886 848 C 876 857 871 837 859 845 C 855 848 856 853 856 858 C 856 869 852 871 849 880 C 848 884 848 889 845 893 C 838 904 826 902 823 916 z" id="_kie"/> + <path class="nopower" d="M 822 920 C 820 928 819 933 815 940 C 813 943 810 948 810 951 C 809 957 817 969 815 978 C 813 984 810 984 808 988 C 805 998 813 1013 817 1015 C 821 1017 831 1013 834 1010 C 838 1007 841 1000 844 996 C 849 989 856 980 864 976 C 873 971 877 974 884 971 C 886 970 890 968 891 966 C 892 962 882 952 879 949 C 863 933 857 932 837 925 C 837 925 822 920 822 920 z" id="_ruh"/> + <path class="nopower" d="M 820 1020 C 820 1020 824 1031 824 1031 C 824 1031 821 1045 821 1045 C 821 1045 819 1058 819 1058 C 829 1058 833 1058 843 1057 C 847 1056 855 1054 859 1054 C 865 1055 868 1059 872 1060 C 875 1062 879 1060 883 1062 C 887 1064 889 1067 894 1068 C 894 1068 919 1066 919 1066 C 932 1066 937 1066 950 1069 C 950 1065 948 1059 950 1055 C 952 1049 956 1049 960 1047 C 967 1043 966 1040 975 1039 C 974 1033 971 1029 967 1024 C 967 1024 948 1001 948 1001 C 942 993 938 985 947 976 C 949 975 952 973 954 971 C 958 969 962 968 966 966 C 966 966 957 936 957 936 C 953 937 939 942 935 944 C 935 944 924 950 924 950 C 919 953 913 953 908 957 C 900 962 895 969 887 973 C 876 979 871 974 859 984 C 848 992 845 1004 837 1012 C 832 1017 826 1017 820 1020 z" id="_mun"/> + <path class="nopower" d="M 1277 1053 C 1276 1059 1276 1068 1272 1073 C 1268 1079 1259 1080 1258 1086 C 1257 1092 1263 1097 1266 1101 C 1276 1111 1287 1119 1280 1134 C 1275 1143 1265 1147 1256 1150 C 1256 1150 1203 1163 1203 1163 C 1193 1167 1180 1178 1187 1190 C 1189 1192 1192 1193 1194 1195 C 1198 1197 1198 1200 1201 1201 C 1205 1203 1226 1205 1231 1205 C 1242 1205 1255 1206 1266 1204 C 1266 1204 1287 1197 1287 1197 C 1303 1194 1323 1194 1339 1199 C 1339 1193 1341 1177 1343 1172 C 1346 1167 1352 1163 1355 1158 C 1357 1154 1357 1150 1357 1145 C 1349 1146 1336 1151 1329 1148 C 1321 1143 1317 1130 1317 1122 C 1317 1122 1318 1110 1318 1110 C 1318 1096 1314 1093 1307 1082 C 1299 1069 1293 1057 1277 1053 z" id="_rum"/> + <path class="nopower" d="M 1188 1199 C 1187 1205 1185 1210 1187 1216 C 1189 1223 1196 1231 1196 1237 C 1197 1245 1189 1244 1190 1255 C 1193 1268 1204 1282 1189 1291 C 1195 1299 1199 1297 1208 1294 C 1208 1294 1224 1288 1224 1288 C 1236 1284 1244 1282 1249 1297 C 1250 1301 1250 1303 1250 1307 C 1250 1307 1250 1311 1250 1311 C 1250 1311 1269 1311 1269 1311 C 1272 1311 1276 1313 1279 1311 C 1281 1310 1282 1308 1283 1306 C 1292 1293 1281 1282 1293 1270 C 1306 1258 1316 1264 1328 1262 C 1328 1262 1320 1248 1320 1248 C 1319 1245 1321 1241 1322 1238 C 1325 1229 1323 1223 1326 1218 C 1331 1210 1335 1216 1338 1206 C 1324 1199 1299 1201 1284 1204 C 1276 1206 1273 1209 1263 1211 C 1263 1211 1242 1212 1242 1212 C 1242 1212 1229 1211 1229 1211 C 1222 1211 1207 1210 1201 1208 C 1193 1206 1195 1202 1188 1199 z" id="_bul"/> + <path class="nopower" d="M 1155 1385 C 1155 1385 1155 1387 1155 1387 C 1153 1388 1149 1389 1148 1391 C 1146 1395 1152 1403 1155 1404 C 1162 1406 1184 1400 1188 1400 C 1191 1401 1193 1402 1196 1403 C 1200 1405 1207 1407 1206 1413 C 1205 1416 1203 1418 1200 1418 C 1200 1418 1191 1413 1191 1413 C 1186 1411 1178 1409 1173 1411 C 1173 1411 1166 1413 1166 1413 C 1162 1414 1160 1414 1158 1416 C 1155 1418 1156 1422 1158 1425 C 1161 1430 1165 1433 1167 1439 C 1167 1439 1172 1459 1172 1459 C 1173 1458 1175 1455 1176 1454 C 1185 1448 1189 1467 1191 1471 C 1193 1468 1195 1461 1199 1461 C 1202 1461 1203 1464 1209 1465 C 1208 1457 1204 1448 1201 1441 C 1199 1438 1197 1435 1201 1432 C 1206 1428 1214 1437 1217 1432 C 1220 1429 1212 1426 1211 1422 C 1210 1418 1213 1415 1217 1415 C 1223 1415 1227 1419 1232 1421 C 1232 1421 1231 1414 1231 1414 C 1230 1409 1232 1407 1228 1403 C 1223 1398 1203 1395 1199 1388 C 1196 1384 1199 1382 1199 1379 C 1200 1376 1198 1374 1198 1372 C 1199 1365 1208 1368 1212 1369 C 1208 1358 1195 1350 1192 1338 C 1190 1329 1198 1325 1201 1326 C 1204 1328 1202 1331 1205 1335 C 1207 1339 1212 1341 1216 1343 C 1216 1343 1214 1336 1214 1336 C 1214 1336 1230 1344 1230 1344 C 1230 1344 1222 1331 1222 1331 C 1222 1331 1233 1332 1233 1332 C 1231 1330 1226 1324 1226 1322 C 1225 1319 1229 1318 1231 1317 C 1237 1315 1235 1313 1243 1313 C 1243 1307 1245 1296 1238 1292 C 1234 1291 1222 1296 1218 1297 C 1218 1297 1187 1308 1187 1308 C 1187 1308 1166 1313 1166 1313 C 1164 1314 1159 1315 1158 1316 C 1155 1318 1156 1323 1156 1326 C 1156 1331 1154 1335 1151 1339 C 1146 1344 1141 1345 1138 1349 C 1136 1351 1135 1353 1134 1355 C 1132 1358 1130 1360 1129 1363 C 1127 1371 1134 1379 1140 1382 C 1145 1385 1145 1381 1155 1385 z M 1208 1382 C 1211 1386 1222 1396 1226 1399 C 1229 1400 1232 1401 1235 1401 C 1235 1390 1229 1393 1222 1389 C 1216 1385 1216 1382 1208 1382 z" id="_gre"/> + <path class="nopower" d="M 1636 1288 C 1625 1299 1615 1296 1600 1296 C 1584 1296 1573 1300 1561 1311 C 1546 1324 1537 1340 1516 1344 C 1513 1344 1510 1345 1507 1345 C 1498 1344 1483 1338 1475 1340 C 1470 1341 1465 1346 1461 1348 C 1451 1355 1437 1365 1425 1367 C 1418 1369 1409 1364 1401 1365 C 1395 1365 1391 1368 1386 1368 C 1379 1368 1376 1365 1371 1365 C 1366 1364 1362 1367 1358 1368 C 1358 1368 1349 1368 1349 1368 C 1342 1369 1337 1368 1331 1364 C 1328 1362 1323 1358 1319 1358 C 1316 1359 1311 1365 1311 1368 C 1311 1371 1313 1373 1313 1376 C 1314 1379 1312 1381 1311 1384 C 1311 1388 1312 1390 1311 1393 C 1311 1396 1307 1399 1305 1401 C 1312 1403 1321 1406 1322 1414 C 1322 1419 1320 1417 1323 1428 C 1323 1428 1331 1427 1331 1427 C 1333 1434 1333 1433 1330 1440 C 1330 1440 1349 1436 1349 1436 C 1351 1436 1354 1437 1355 1439 C 1355 1442 1350 1448 1349 1451 C 1355 1449 1355 1447 1359 1446 C 1363 1445 1366 1447 1370 1447 C 1372 1448 1375 1447 1377 1448 C 1381 1450 1383 1454 1385 1457 C 1389 1460 1395 1462 1400 1462 C 1404 1462 1416 1458 1418 1455 C 1421 1450 1418 1435 1432 1432 C 1443 1431 1460 1441 1469 1446 C 1474 1449 1478 1452 1484 1452 C 1493 1452 1512 1447 1518 1441 C 1524 1435 1528 1424 1535 1420 C 1544 1415 1552 1425 1558 1422 C 1565 1418 1564 1405 1575 1408 C 1579 1402 1583 1394 1586 1388 C 1590 1375 1588 1369 1603 1358 C 1611 1353 1614 1353 1622 1350 C 1622 1350 1640 1342 1640 1342 C 1644 1340 1652 1339 1654 1335 C 1657 1322 1641 1315 1637 1308 C 1634 1303 1637 1293 1636 1288 z" id="_smy"/> + <path class="nopower" d="M 1149 1316 C 1136 1313 1137 1308 1136 1297 C 1134 1284 1137 1271 1125 1262 C 1123 1261 1120 1259 1118 1258 C 1112 1257 1101 1264 1101 1273 C 1100 1280 1110 1281 1112 1290 C 1113 1298 1107 1314 1107 1326 C 1107 1329 1107 1334 1108 1337 C 1109 1341 1121 1352 1125 1353 C 1130 1354 1128 1349 1133 1344 C 1137 1340 1142 1339 1145 1336 C 1151 1331 1150 1323 1149 1316 z" id="_alb"/> + <path class="nopower" d="M 1189 1301 C 1187 1298 1181 1293 1183 1288 C 1185 1285 1189 1285 1190 1280 C 1191 1274 1184 1263 1184 1254 C 1184 1251 1184 1248 1185 1245 C 1187 1242 1190 1240 1190 1236 C 1189 1231 1184 1226 1181 1218 C 1178 1209 1181 1208 1181 1200 C 1181 1196 1181 1185 1177 1183 C 1176 1182 1173 1182 1171 1182 C 1171 1182 1144 1180 1144 1180 C 1137 1179 1135 1175 1131 1176 C 1128 1176 1125 1179 1121 1181 C 1115 1183 1111 1176 1107 1181 C 1106 1181 1104 1184 1104 1185 C 1100 1194 1107 1196 1107 1204 C 1106 1208 1104 1212 1103 1216 C 1101 1222 1101 1232 1104 1238 C 1110 1249 1126 1253 1134 1262 C 1140 1269 1140 1274 1141 1283 C 1141 1283 1142 1292 1142 1292 C 1142 1296 1141 1303 1144 1307 C 1146 1309 1151 1309 1154 1309 C 1165 1309 1174 1302 1189 1301 z" id="_ser"/> + <path class="nopower" d="M 1121 1029 C 1121 1031 1121 1033 1120 1035 C 1118 1041 1113 1038 1109 1039 C 1106 1040 1104 1041 1102 1043 C 1092 1052 1092 1061 1085 1069 C 1079 1075 1074 1076 1069 1080 C 1065 1083 1060 1089 1056 1093 C 1052 1097 1047 1100 1047 1106 C 1047 1106 1048 1113 1048 1113 C 1050 1124 1054 1132 1063 1139 C 1069 1143 1075 1144 1079 1148 C 1088 1155 1088 1174 1103 1173 C 1103 1173 1111 1171 1111 1171 C 1113 1172 1115 1174 1117 1174 C 1123 1176 1125 1169 1132 1169 C 1136 1170 1139 1173 1144 1174 C 1151 1175 1155 1175 1162 1175 C 1162 1175 1169 1176 1169 1176 C 1181 1177 1183 1170 1191 1163 C 1196 1160 1200 1157 1206 1156 C 1206 1156 1258 1143 1258 1143 C 1268 1139 1276 1135 1275 1123 C 1274 1113 1259 1104 1253 1095 C 1253 1095 1249 1087 1249 1087 C 1249 1087 1234 1065 1234 1065 C 1230 1061 1227 1062 1221 1056 C 1213 1047 1208 1039 1197 1033 C 1187 1028 1182 1031 1173 1028 C 1166 1026 1162 1021 1154 1021 C 1144 1021 1131 1027 1121 1029 z" id="_bud"/> + <path class="nopower" d="M 1083 997 C 1083 997 1085 1015 1085 1015 C 1094 1015 1098 1015 1106 1018 C 1112 1021 1115 1025 1120 1025 C 1123 1025 1128 1023 1131 1022 C 1139 1020 1146 1018 1155 1018 C 1164 1018 1166 1023 1174 1025 C 1182 1027 1189 1024 1201 1031 C 1212 1038 1216 1046 1224 1054 C 1229 1059 1232 1058 1238 1065 C 1243 1071 1246 1078 1252 1083 C 1258 1073 1266 1074 1269 1066 C 1270 1062 1269 1047 1269 1042 C 1269 1030 1262 1010 1254 1001 C 1249 996 1240 993 1234 988 C 1229 985 1220 975 1216 974 C 1209 973 1205 980 1193 981 C 1186 981 1180 978 1174 975 C 1172 974 1166 971 1163 971 C 1160 971 1157 976 1155 978 C 1151 982 1145 985 1140 988 C 1134 990 1130 990 1124 989 C 1121 989 1113 987 1111 987 C 1108 988 1105 991 1100 994 C 1096 996 1088 997 1083 997 z" id="_gal"/> + <path class="nopower" d="M 1117 1036 C 1117 1034 1118 1031 1117 1029 C 1116 1028 1113 1026 1111 1025 C 1104 1021 1099 1019 1091 1019 C 1091 1019 1074 1020 1074 1020 C 1067 1019 1069 1016 1057 1016 C 1057 1016 1045 1018 1045 1018 C 1040 1018 1033 1013 1025 1019 C 1020 1024 1016 1031 1012 1037 C 1010 1042 1005 1049 1003 1054 C 1001 1061 1002 1078 1005 1085 C 1006 1088 1007 1090 1009 1092 C 1012 1097 1017 1107 1023 1108 C 1027 1108 1031 1104 1035 1102 C 1035 1102 1044 1100 1044 1100 C 1052 1096 1059 1084 1065 1078 C 1072 1072 1078 1073 1084 1064 C 1087 1060 1088 1057 1090 1053 C 1093 1048 1101 1037 1107 1036 C 1110 1035 1114 1036 1117 1036 z" id="_vie"/> + <path class="nopower" d="M 1081 1016 C 1081 1016 1079 997 1079 997 C 1079 997 1083 997 1083 997 C 1083 997 1075 995 1075 995 C 1075 995 1063 990 1063 990 C 1056 988 1053 988 1046 984 C 1034 977 1023 959 1008 961 C 999 963 971 970 963 974 C 959 976 952 979 950 983 C 947 988 950 993 953 997 C 953 997 977 1027 977 1027 C 979 1031 981 1039 986 1040 C 990 1041 995 1041 999 1041 C 1001 1041 1004 1042 1005 1041 C 1008 1040 1011 1033 1012 1031 C 1017 1023 1025 1011 1036 1012 C 1040 1013 1042 1014 1046 1014 C 1050 1014 1053 1012 1060 1012 C 1067 1012 1069 1015 1073 1016 C 1075 1017 1079 1016 1081 1016 z" id="_boh"/> + <path class="nopower" d="M 875 1067 C 878 1085 889 1086 894 1098 C 898 1108 892 1109 892 1122 C 892 1129 895 1137 903 1135 C 908 1135 915 1130 917 1125 C 918 1122 917 1119 920 1115 C 926 1109 936 1108 944 1108 C 947 1108 957 1108 959 1107 C 961 1106 962 1104 964 1102 C 967 1100 970 1099 973 1099 C 977 1098 981 1098 985 1099 C 988 1099 991 1100 994 1099 C 998 1097 1001 1088 1000 1084 C 999 1065 995 1064 1003 1045 C 994 1045 981 1042 973 1046 C 970 1047 967 1050 964 1052 C 962 1053 958 1054 957 1056 C 952 1060 962 1072 952 1075 C 950 1076 946 1075 944 1074 C 939 1073 936 1073 931 1073 C 931 1073 910 1073 910 1073 C 904 1073 897 1075 891 1074 C 884 1073 885 1068 875 1067 z" id="_tyr"/> + <path class="nopower" d="M 1003 1091 C 1001 1096 998 1102 993 1103 C 987 1104 968 1097 964 1109 C 966 1110 968 1112 970 1114 C 973 1120 969 1134 967 1140 C 966 1145 968 1144 965 1152 C 964 1157 962 1168 969 1169 C 972 1170 975 1165 976 1163 C 980 1156 980 1153 989 1154 C 989 1157 989 1159 988 1162 C 986 1168 982 1170 986 1181 C 990 1193 1002 1199 1011 1207 C 1016 1212 1018 1214 1024 1219 C 1024 1219 1047 1234 1047 1234 C 1047 1234 1064 1248 1064 1248 C 1064 1248 1081 1260 1081 1260 C 1086 1263 1090 1268 1095 1270 C 1097 1260 1102 1257 1110 1253 C 1101 1243 1094 1239 1095 1224 C 1095 1214 1099 1210 1100 1205 C 1100 1200 1097 1197 1096 1192 C 1096 1187 1099 1182 1100 1177 C 1095 1175 1091 1174 1088 1169 C 1084 1164 1083 1156 1078 1151 C 1073 1147 1068 1148 1059 1140 C 1047 1131 1046 1119 1043 1105 C 1039 1105 1037 1105 1034 1107 C 1031 1108 1027 1112 1023 1112 C 1014 1111 1010 1098 1003 1091 z" id="_tri"/> + <path class="nopower" d="M 1181 440 C 1190 443 1204 455 1199 465 C 1199 465 1189 478 1189 478 C 1189 478 1175 500 1175 500 C 1175 500 1153 527 1153 527 C 1148 534 1139 544 1140 553 C 1140 553 1142 562 1142 562 C 1142 562 1143 572 1143 572 C 1143 572 1147 586 1147 586 C 1147 593 1142 600 1142 608 C 1142 612 1142 618 1146 622 C 1148 624 1151 624 1154 626 C 1154 626 1163 631 1163 631 C 1163 631 1170 633 1170 633 C 1175 635 1176 639 1181 640 C 1186 640 1200 636 1205 635 C 1205 635 1238 622 1238 622 C 1244 620 1258 617 1262 614 C 1268 611 1271 605 1275 599 C 1282 588 1290 572 1289 559 C 1289 555 1288 553 1288 549 C 1289 539 1294 537 1294 528 C 1292 512 1283 498 1276 483 C 1269 468 1260 443 1255 427 C 1252 417 1250 405 1249 394 C 1249 389 1248 376 1247 372 C 1246 367 1241 364 1237 357 C 1234 350 1232 341 1232 333 C 1228 336 1224 334 1224 329 C 1223 325 1227 318 1227 312 C 1226 301 1208 291 1199 299 C 1196 302 1196 308 1195 312 C 1193 323 1191 330 1179 332 C 1171 334 1157 331 1149 328 C 1149 328 1139 323 1139 323 C 1135 322 1132 323 1131 327 C 1130 332 1134 334 1137 337 C 1145 343 1153 350 1159 358 C 1171 374 1173 391 1177 410 C 1177 410 1181 440 1181 440 z" id="_fin"/> + <path class="nopower" d="M 1128 338 C 1128 338 1126 348 1126 348 C 1120 349 1118 350 1112 351 C 1109 351 1104 351 1101 352 C 1098 355 1101 358 1098 362 C 1095 366 1091 362 1087 367 C 1082 373 1075 395 1071 403 C 1068 409 1064 416 1059 422 C 1055 428 1052 430 1048 437 C 1048 437 1043 456 1043 456 C 1043 456 1032 478 1032 478 C 1032 478 1028 494 1028 494 C 1023 504 1015 499 1008 505 C 1006 508 1004 513 1003 517 C 999 529 999 540 999 552 C 999 552 1000 567 1000 567 C 1000 567 1000 581 1000 581 C 1000 586 1000 589 998 593 C 994 601 991 599 991 610 C 991 623 987 636 984 649 C 981 657 978 676 970 679 C 967 680 964 679 961 679 C 961 689 964 693 966 703 C 966 703 973 731 973 731 C 975 736 981 751 982 755 C 982 760 978 769 979 774 C 979 774 982 786 982 786 C 984 794 980 803 993 801 C 1005 800 1003 794 1009 786 C 1014 780 1019 778 1026 778 C 1030 778 1037 779 1041 777 C 1045 774 1048 768 1049 764 C 1055 750 1054 741 1055 727 C 1055 727 1060 704 1060 704 C 1060 702 1060 699 1059 697 C 1058 696 1056 694 1057 692 C 1057 690 1060 689 1062 689 C 1067 688 1072 687 1077 684 C 1086 680 1090 674 1094 665 C 1097 659 1099 657 1099 650 C 1098 635 1083 627 1078 620 C 1075 616 1074 610 1074 605 C 1074 592 1078 571 1083 559 C 1086 553 1095 542 1099 537 C 1112 523 1128 517 1139 498 C 1144 489 1138 486 1139 477 C 1140 464 1149 444 1162 439 C 1166 438 1170 438 1174 438 C 1174 438 1171 417 1171 417 C 1169 402 1165 380 1158 367 C 1151 355 1139 345 1128 338 z" id="_swe"/> + <path class="nopower" d="M 1198 263 C 1194 266 1193 270 1192 274 C 1189 282 1188 287 1182 292 C 1180 283 1183 280 1183 272 C 1183 270 1183 267 1181 266 C 1178 265 1175 267 1173 268 C 1166 275 1167 275 1158 280 C 1158 280 1133 298 1133 298 C 1128 300 1123 302 1118 303 C 1115 304 1111 304 1108 305 C 1104 307 1104 312 1100 315 C 1097 317 1095 316 1093 317 C 1089 319 1087 324 1086 328 C 1088 330 1090 332 1089 335 C 1088 337 1083 342 1081 344 C 1081 344 1063 367 1063 367 C 1063 367 1045 394 1045 394 C 1045 394 1024 415 1024 415 C 1019 422 1015 431 1010 439 C 1005 447 995 464 992 472 C 992 472 991 483 991 483 C 983 480 980 483 974 488 C 971 491 967 496 965 499 C 964 502 964 506 961 508 C 958 510 952 509 947 512 C 941 515 937 521 933 522 C 929 523 928 521 925 521 C 921 520 918 522 916 526 C 916 526 920 528 920 528 C 915 534 912 532 905 535 C 906 537 908 541 907 542 C 905 545 900 541 894 542 C 888 544 882 551 879 556 C 871 567 869 577 867 590 C 866 593 865 601 865 604 C 867 609 872 610 874 614 C 876 619 871 620 867 621 C 867 621 871 624 871 624 C 879 633 863 634 861 637 C 860 638 860 642 860 644 C 864 643 878 639 879 646 C 879 650 874 653 872 655 C 865 660 859 665 864 674 C 866 677 869 681 872 683 C 874 685 877 686 880 688 C 899 697 913 679 929 672 C 934 669 939 668 944 664 C 946 663 950 658 953 658 C 957 658 960 668 961 672 C 964 672 968 674 970 671 C 972 668 974 659 975 656 C 978 646 984 626 984 616 C 984 612 984 606 985 602 C 987 597 991 595 993 589 C 994 584 994 578 994 573 C 994 573 992 546 992 546 C 992 536 995 516 1000 507 C 1002 502 1006 498 1011 496 C 1016 494 1020 495 1022 491 C 1022 491 1026 476 1026 476 C 1026 476 1037 453 1037 453 C 1037 453 1043 434 1043 434 C 1046 427 1053 421 1058 414 C 1067 400 1070 386 1077 372 C 1081 364 1084 358 1093 357 C 1092 343 1110 344 1120 344 C 1121 340 1122 336 1123 332 C 1124 326 1127 318 1134 316 C 1139 315 1144 319 1149 321 C 1156 324 1165 326 1172 326 C 1176 326 1181 326 1184 323 C 1191 317 1185 298 1199 291 C 1201 290 1203 290 1205 290 C 1217 289 1232 299 1234 312 C 1234 317 1231 324 1230 330 C 1241 325 1247 308 1251 297 C 1249 298 1243 300 1241 299 C 1237 299 1229 292 1227 288 C 1232 287 1244 285 1246 281 C 1251 273 1234 268 1229 268 C 1222 269 1223 277 1219 279 C 1216 280 1215 277 1214 275 C 1214 271 1218 257 1210 259 C 1203 260 1207 278 1201 280 C 1197 281 1197 275 1197 273 C 1197 273 1198 263 1198 263 z" id="_nwy"/> + <path class="nopower" d="M 939 716 C 933 720 923 725 917 728 C 912 729 904 731 900 734 C 896 737 895 742 894 746 C 892 754 888 769 889 777 C 891 785 894 784 894 796 C 899 796 912 796 916 798 C 919 794 919 789 920 784 C 922 779 927 774 930 770 C 932 768 934 767 936 766 C 938 766 942 768 943 763 C 943 757 933 755 934 747 C 934 743 938 738 940 734 C 943 728 943 721 939 716 z M 946 786 C 951 798 951 796 959 805 C 962 809 962 810 967 813 C 967 813 969 804 969 804 C 971 802 974 800 974 797 C 975 795 973 791 972 788 C 970 782 972 777 964 775 C 963 775 959 775 958 775 C 954 776 955 780 946 786 z M 944 812 C 943 813 943 815 943 816 C 942 826 959 823 952 815 C 951 815 950 814 949 814 C 947 813 946 812 944 812 z" id="_den"/> + <path class="nopower" d="M 768 906 C 778 917 791 914 799 922 C 804 928 800 932 804 943 C 804 943 806 943 806 943 C 809 937 813 931 815 924 C 816 918 817 911 820 906 C 824 900 834 897 839 892 C 841 889 841 883 843 879 C 847 866 850 867 850 850 C 850 850 823 849 823 849 C 815 850 801 861 796 866 C 786 876 790 890 780 900 C 776 903 772 904 768 906 z" id="_hol"/> + <path class="nopower" d="M 715 931 C 715 931 747 950 747 950 C 760 958 761 961 777 969 C 782 972 798 980 804 980 C 809 979 808 972 807 968 C 805 954 798 945 796 940 C 794 934 796 929 793 926 C 791 923 785 922 782 921 C 782 921 775 918 775 918 C 769 915 766 914 761 908 C 754 910 745 912 738 913 C 733 914 726 914 722 917 C 718 921 717 926 715 931 z" id="_bel"/> + <path class="impassable" d="M 778 1102 C 786 1104 791 1096 798 1099 C 809 1103 801 1114 814 1113 C 822 1111 825 1108 834 1112 C 842 1114 846 1121 851 1120 C 856 1120 857 1113 865 1112 C 870 1111 883 1114 886 1111 C 890 1109 890 1101 888 1098 C 887 1097 885 1094 884 1093 C 880 1089 877 1086 874 1081 C 868 1070 869 1061 853 1061 C 853 1061 827 1063 827 1063 C 823 1063 818 1063 814 1065 C 811 1066 797 1081 793 1085 C 793 1085 786 1092 786 1092 C 782 1096 780 1095 778 1102 z" id="_swi"/> + <path class="nopower" d="M 957 1271 C 959 1266 960 1265 964 1260 C 966 1258 969 1255 969 1252 C 969 1250 965 1243 964 1240 C 959 1228 962 1222 951 1210 C 941 1199 927 1193 928 1176 C 928 1168 932 1168 933 1164 C 934 1160 928 1155 935 1150 C 938 1148 949 1143 952 1142 C 955 1141 959 1141 961 1139 C 962 1137 964 1125 965 1122 C 965 1120 965 1118 963 1117 C 961 1115 950 1114 947 1114 C 942 1114 929 1115 926 1119 C 924 1121 924 1123 923 1126 C 918 1134 910 1143 900 1142 C 894 1141 892 1136 888 1136 C 886 1136 883 1139 881 1141 C 877 1146 873 1153 872 1160 C 872 1167 879 1174 884 1178 C 891 1184 897 1186 903 1190 C 912 1196 920 1205 926 1213 C 934 1224 937 1231 942 1244 C 942 1244 945 1260 945 1260 C 946 1264 953 1269 957 1271 z" id="_ven"/> + <path class="nopower" d="M 804 1114 C 803 1117 801 1120 802 1124 C 803 1128 807 1130 806 1135 C 805 1138 801 1142 801 1146 C 800 1151 804 1156 805 1161 C 806 1168 800 1176 806 1184 C 807 1186 809 1188 811 1189 C 815 1191 823 1187 826 1184 C 832 1179 839 1173 848 1174 C 856 1175 860 1182 869 1186 C 871 1183 875 1178 874 1175 C 874 1171 869 1169 869 1161 C 869 1149 878 1138 887 1131 C 887 1131 886 1119 886 1119 C 886 1119 866 1116 866 1116 C 859 1117 860 1129 851 1127 C 844 1126 838 1116 830 1115 C 821 1114 814 1125 804 1114 z" id="_pie"/> + <path class="nopower" d="M 878 1178 C 877 1180 873 1185 873 1188 C 873 1191 875 1194 876 1201 C 877 1208 877 1215 878 1221 C 882 1232 891 1245 899 1253 C 899 1253 914 1239 914 1239 C 914 1239 932 1229 932 1229 C 927 1220 918 1207 911 1200 C 901 1191 899 1192 889 1186 C 889 1186 878 1178 878 1178 z" id="_tus"/> + <path class="nopower" d="M 973 1291 C 973 1291 951 1271 951 1271 C 951 1271 942 1262 942 1262 C 942 1262 939 1249 939 1249 C 939 1249 934 1233 934 1233 C 925 1235 916 1241 909 1246 C 907 1249 903 1252 903 1256 C 903 1261 911 1268 914 1272 C 921 1280 926 1290 936 1295 C 941 1297 945 1296 950 1297 C 956 1298 958 1301 961 1300 C 961 1300 973 1291 973 1291 z" id="_rom"/> + <path class="nopower" d="M 972 1256 C 968 1260 960 1267 960 1272 C 960 1277 967 1281 969 1283 C 969 1283 991 1300 991 1300 C 996 1304 1005 1314 1008 1320 C 1011 1326 1010 1330 1013 1334 C 1015 1337 1028 1344 1032 1345 C 1034 1342 1038 1334 1041 1332 C 1042 1330 1043 1329 1046 1330 C 1046 1330 1052 1335 1052 1335 C 1057 1339 1063 1343 1069 1346 C 1072 1347 1077 1351 1079 1348 C 1081 1345 1079 1339 1077 1336 C 1074 1331 1061 1323 1055 1319 C 1044 1312 1029 1303 1021 1294 C 1019 1292 1018 1290 1017 1287 C 1017 1285 1018 1282 1016 1280 C 1015 1279 1010 1279 1008 1279 C 1003 1278 999 1277 995 1275 C 984 1270 980 1262 972 1256 z" id="_apu"/> + <path class="nopower" d="M 1029 1348 C 1024 1345 1013 1339 1009 1335 C 1006 1331 1007 1326 1005 1321 C 1002 1314 992 1305 986 1300 C 982 1297 978 1294 973 1296 C 969 1297 962 1303 962 1307 C 962 1310 964 1314 966 1316 C 966 1318 968 1321 969 1322 C 972 1326 979 1322 984 1326 C 988 1329 986 1333 991 1340 C 996 1346 1004 1350 1008 1357 C 1010 1360 1017 1377 1017 1381 C 1018 1383 1018 1385 1017 1387 C 1017 1389 1009 1402 1008 1404 C 1005 1407 1001 1412 1004 1417 C 1007 1421 1012 1419 1016 1416 C 1019 1413 1024 1405 1025 1400 C 1026 1397 1027 1392 1029 1389 C 1032 1385 1037 1387 1039 1385 C 1042 1383 1042 1378 1042 1375 C 1041 1362 1030 1364 1028 1356 C 1027 1354 1028 1350 1029 1348 z" id="_nap"/> + <path class="nopower" d="M 773 975 C 769 985 759 1008 751 1015 C 747 1018 745 1018 742 1021 C 742 1021 733 1028 733 1028 C 733 1028 725 1033 725 1033 C 721 1036 720 1039 717 1041 C 713 1044 710 1043 707 1046 C 702 1052 703 1058 701 1064 C 699 1073 692 1083 683 1086 C 683 1090 684 1093 688 1096 C 692 1099 699 1098 702 1103 C 704 1107 701 1113 700 1118 C 700 1128 713 1145 724 1138 C 731 1134 735 1120 737 1112 C 737 1108 736 1100 740 1098 C 743 1096 746 1097 749 1098 C 754 1100 759 1101 764 1101 C 766 1101 770 1101 772 1100 C 772 1100 785 1086 785 1086 C 791 1080 807 1065 811 1058 C 813 1054 813 1049 815 1044 C 815 1044 818 1034 818 1034 C 818 1028 812 1019 808 1014 C 802 1003 800 998 802 986 C 802 986 773 975 773 975 z" id="_bur"/> + <path class="nopower" d="M 741 1100 C 741 1112 739 1122 733 1133 C 730 1138 727 1142 721 1143 C 715 1144 712 1141 707 1139 C 704 1143 700 1147 695 1148 C 691 1150 682 1148 677 1153 C 674 1155 670 1164 667 1168 C 667 1168 650 1192 650 1192 C 654 1197 668 1205 674 1208 C 677 1209 682 1211 685 1211 C 690 1210 686 1204 690 1196 C 693 1189 703 1182 711 1181 C 721 1180 725 1187 733 1189 C 738 1190 743 1188 748 1191 C 754 1196 756 1207 768 1208 C 781 1210 791 1196 803 1193 C 801 1186 797 1185 797 1176 C 797 1176 798 1161 798 1161 C 797 1157 794 1153 794 1147 C 794 1140 800 1137 799 1133 C 799 1130 795 1127 794 1123 C 794 1117 800 1112 798 1108 C 797 1105 792 1106 789 1107 C 781 1109 778 1112 773 1104 C 760 1107 753 1102 741 1100 z" id="_mar"/> + <path class="nopower" d="M 611 1058 C 611 1058 611 1079 611 1079 C 611 1079 614 1086 614 1086 C 614 1086 609 1084 609 1084 C 606 1088 601 1097 601 1102 C 600 1105 601 1107 601 1110 C 600 1113 591 1131 589 1134 C 587 1138 584 1141 580 1145 C 578 1146 576 1148 576 1151 C 577 1155 588 1165 592 1168 C 600 1173 611 1180 620 1183 C 620 1183 633 1185 633 1185 C 637 1186 643 1189 647 1191 C 647 1191 664 1167 664 1167 C 667 1162 671 1153 676 1149 C 682 1145 688 1147 695 1145 C 699 1143 702 1140 705 1137 C 702 1132 698 1128 697 1122 C 696 1116 699 1110 700 1104 C 695 1102 688 1101 684 1098 C 680 1094 681 1090 677 1087 C 675 1086 670 1085 663 1080 C 658 1076 655 1070 650 1067 C 643 1063 620 1059 611 1058 z" id="_gas"/> + <path class="nopower" d="M 711 936 C 707 940 703 944 698 946 C 694 947 692 947 688 947 C 684 948 680 950 677 952 C 677 952 677 954 677 954 C 677 954 681 956 681 956 C 681 956 681 958 681 958 C 673 960 670 959 667 960 C 664 961 662 975 661 979 C 673 979 687 980 698 977 C 705 976 709 972 715 972 C 721 971 728 975 733 977 C 749 983 743 978 763 989 C 763 989 770 972 770 972 C 759 967 755 962 745 956 C 745 956 711 936 711 936 z" id="_pic"/> + <path class="nopower" d="M 661 980 C 657 990 660 1003 660 1014 C 660 1035 658 1046 654 1066 C 658 1069 658 1071 661 1074 C 666 1079 675 1084 682 1082 C 690 1081 695 1072 697 1065 C 699 1058 698 1052 703 1045 C 708 1039 711 1041 716 1038 C 716 1038 722 1031 722 1031 C 722 1031 732 1025 732 1025 C 732 1025 740 1018 740 1018 C 744 1015 746 1015 750 1012 C 753 1008 759 997 761 992 C 745 983 747 986 733 981 C 728 979 720 975 715 975 C 709 976 706 979 701 980 C 693 983 684 982 676 982 C 671 982 665 983 661 980 z" id="_par"/> + <path class="nopower" d="M 631 936 C 631 936 633 952 633 952 C 633 952 631 966 631 966 C 631 966 633 976 633 976 C 626 975 627 974 621 971 C 621 971 601 963 601 963 C 597 961 595 957 592 956 C 589 955 586 956 582 956 C 576 955 564 953 559 958 C 557 959 556 962 555 965 C 553 967 550 969 550 973 C 552 980 562 979 567 982 C 573 985 586 997 591 1002 C 595 1007 593 1008 595 1011 C 596 1013 598 1015 599 1019 C 601 1024 599 1027 599 1031 C 600 1037 605 1048 610 1052 C 613 1055 619 1056 623 1056 C 623 1056 651 1063 651 1063 C 653 1053 656 1036 656 1026 C 656 1026 656 1009 656 1009 C 656 1009 655 999 655 999 C 655 982 659 974 662 958 C 656 956 651 954 647 950 C 642 945 644 941 641 939 C 640 937 634 936 631 936 z M 553 968 C 553 968 553 969 553 969 C 553 969 552 968 552 968 C 552 968 553 968 553 968 z" id="_bre"/> + <path class="nopower" d="M 386 1138 C 391 1137 398 1136 403 1138 C 407 1141 408 1144 414 1146 C 424 1149 439 1146 447 1159 C 449 1162 450 1166 450 1169 C 448 1177 442 1174 434 1184 C 434 1184 415 1216 415 1216 C 413 1219 410 1224 407 1226 C 405 1228 401 1230 400 1232 C 399 1235 399 1243 399 1246 C 399 1257 395 1257 393 1265 C 392 1272 396 1276 391 1284 C 388 1289 383 1291 379 1295 C 375 1300 372 1308 370 1314 C 379 1319 387 1329 389 1339 C 391 1347 388 1357 397 1362 C 404 1366 407 1361 413 1359 C 418 1358 420 1361 427 1361 C 427 1361 445 1357 445 1357 C 454 1357 462 1363 470 1365 C 478 1368 478 1367 485 1367 C 491 1366 497 1370 501 1369 C 507 1368 509 1360 520 1355 C 527 1352 541 1353 545 1350 C 545 1350 565 1326 565 1326 C 569 1323 575 1322 579 1321 C 577 1315 572 1310 572 1303 C 573 1298 577 1294 580 1290 C 584 1285 588 1276 592 1271 C 592 1271 599 1266 599 1266 C 604 1260 605 1257 613 1257 C 613 1255 613 1253 615 1251 C 618 1247 631 1245 636 1245 C 650 1243 675 1237 683 1225 C 685 1222 686 1220 686 1217 C 671 1217 656 1204 643 1196 C 640 1195 635 1192 632 1191 C 628 1191 626 1192 621 1190 C 613 1188 604 1183 597 1179 C 591 1175 583 1169 578 1164 C 578 1164 567 1151 567 1151 C 567 1151 558 1148 558 1148 C 553 1145 552 1143 548 1141 C 544 1139 540 1141 533 1138 C 533 1138 522 1132 522 1132 C 516 1130 514 1131 506 1127 C 506 1127 472 1110 472 1110 C 472 1110 448 1101 448 1101 C 439 1095 439 1088 427 1089 C 417 1089 419 1094 414 1096 C 409 1098 403 1095 400 1095 C 398 1095 396 1096 394 1098 C 385 1105 390 1114 390 1123 C 390 1129 388 1133 386 1138 z" id="_spa"/> + <path class="nopower" d="M 394 1143 C 391 1144 387 1144 385 1146 C 383 1148 381 1158 380 1161 C 378 1169 372 1180 369 1187 C 365 1194 356 1210 351 1215 C 346 1220 340 1218 336 1224 C 335 1227 332 1240 332 1243 C 332 1245 332 1248 333 1249 C 335 1253 343 1251 343 1259 C 343 1264 340 1269 338 1273 C 336 1280 339 1280 334 1288 C 332 1291 326 1298 327 1302 C 327 1304 337 1309 340 1310 C 344 1312 347 1314 352 1314 C 354 1314 362 1312 363 1311 C 365 1310 369 1298 371 1295 C 379 1282 387 1287 387 1273 C 387 1269 387 1265 388 1261 C 389 1256 392 1254 393 1248 C 394 1242 391 1236 394 1230 C 397 1223 401 1226 409 1215 C 409 1215 429 1180 429 1180 C 439 1168 446 1171 442 1163 C 437 1153 421 1156 412 1152 C 404 1149 404 1143 394 1143 z" id="_por"/> + <path class="nopower" d="M 216 1527 C 216 1527 780 1527 780 1527 C 780 1505 779 1473 783 1452 C 786 1442 790 1439 793 1431 C 784 1430 782 1426 770 1425 C 763 1424 763 1428 757 1427 C 752 1427 746 1422 739 1422 C 736 1422 721 1426 718 1427 C 715 1428 711 1430 708 1429 C 705 1429 701 1425 699 1423 C 695 1420 691 1417 686 1416 C 673 1412 646 1412 632 1412 C 632 1412 612 1410 612 1410 C 602 1410 593 1409 583 1411 C 575 1412 568 1415 561 1418 C 558 1420 552 1423 548 1423 C 544 1423 542 1420 539 1420 C 537 1419 526 1419 524 1420 C 520 1423 516 1427 510 1430 C 501 1434 486 1430 477 1427 C 474 1426 469 1424 466 1422 C 464 1419 463 1417 462 1414 C 457 1415 456 1416 451 1416 C 440 1416 423 1415 416 1406 C 411 1400 409 1391 406 1384 C 405 1381 403 1377 399 1378 C 392 1378 385 1387 381 1392 C 369 1405 360 1419 345 1428 C 332 1437 308 1433 292 1438 C 283 1441 272 1448 265 1454 C 265 1454 256 1465 256 1465 C 256 1465 242 1475 242 1475 C 232 1485 218 1513 216 1527 z" id="_naf"/> + <path class="nopower" d="M 787 1527 C 787 1527 857 1527 857 1527 C 859 1527 863 1527 865 1526 C 867 1524 868 1521 869 1518 C 871 1510 872 1502 868 1494 C 864 1487 861 1486 856 1481 C 853 1478 852 1474 853 1470 C 854 1459 864 1455 869 1450 C 871 1448 873 1442 871 1440 C 869 1438 867 1439 865 1440 C 862 1442 855 1447 851 1446 C 846 1444 849 1438 846 1434 C 842 1429 832 1427 826 1427 C 826 1427 810 1430 810 1430 C 807 1431 804 1431 801 1433 C 795 1438 790 1451 789 1459 C 786 1477 787 1508 787 1527 z" id="_tun"/> + <path class="nopower" d="M 716 901 C 713 899 704 896 702 893 C 699 888 709 883 712 880 C 721 875 735 868 736 856 C 736 854 736 851 735 849 C 731 842 713 838 705 840 C 705 840 694 845 694 845 C 689 847 684 849 679 851 C 679 851 669 853 669 853 C 661 856 655 866 655 875 C 656 883 659 891 664 898 C 665 900 667 903 669 905 C 672 907 677 907 680 908 C 693 910 705 910 716 901 z" id="_lon"/> + <path class="nopower" d="M 612 810 C 624 811 622 824 617 828 C 612 833 594 831 590 837 C 586 844 597 847 601 849 C 608 852 613 860 619 864 C 625 866 626 863 634 865 C 633 867 632 868 630 870 C 619 878 611 870 603 870 C 596 870 596 877 590 872 C 583 878 586 880 571 888 C 565 892 560 890 555 896 C 558 897 561 900 564 900 C 567 900 572 897 575 896 C 582 894 591 897 596 902 C 603 899 605 894 610 892 C 615 891 624 896 630 897 C 630 897 660 900 660 900 C 657 891 650 882 651 872 C 652 864 657 856 664 852 C 668 849 671 849 672 847 C 674 845 672 836 672 832 C 667 831 649 824 645 821 C 642 818 640 815 640 810 C 640 807 640 803 639 800 C 633 792 615 804 612 810 z" id="_wal"/> + <path class="nopower" d="M 622 715 C 622 715 633 711 633 711 C 636 700 641 699 643 706 C 644 715 635 723 630 730 C 624 738 625 742 634 745 C 637 747 636 746 639 746 C 643 747 642 749 645 750 C 647 751 652 751 654 751 C 654 751 647 757 647 757 C 647 757 651 765 651 765 C 651 765 651 778 651 778 C 652 783 654 788 652 793 C 650 803 640 801 644 813 C 644 815 645 817 647 818 C 650 820 668 827 672 828 C 672 813 670 810 672 793 C 672 793 677 777 677 777 C 679 771 678 760 677 754 C 676 748 665 735 661 728 C 654 714 657 711 657 697 C 650 695 633 692 626 692 C 622 693 617 695 616 699 C 615 703 619 703 621 705 C 622 707 622 712 622 715 z" id="_lvp"/> + <path class="nopower" d="M 680 750 C 680 750 681 758 681 758 C 681 758 681 767 681 767 C 681 767 675 804 675 804 C 675 804 677 848 677 848 C 683 846 698 840 703 837 C 703 837 707 832 707 832 C 710 830 714 828 714 824 C 714 818 707 811 704 806 C 711 803 712 802 711 794 C 709 779 702 779 698 768 C 694 761 695 754 692 752 C 690 750 683 750 680 750 z" id="_yor"/> + <path class="nopower" d="M 690 621 C 683 631 677 631 673 638 C 669 644 667 656 666 663 C 666 663 661 699 661 699 C 660 708 659 714 662 723 C 664 727 674 742 677 746 C 683 746 689 748 692 747 C 697 745 697 737 696 733 C 693 724 681 713 687 701 C 691 693 701 684 707 677 C 710 674 714 669 714 665 C 712 657 697 653 690 652 C 687 652 679 654 677 651 C 674 648 681 643 683 642 C 683 642 700 631 700 631 C 706 626 702 623 696 622 C 696 622 690 621 690 621 z" id="_edi"/> + <path class="nopower" d="M 624 681 C 629 681 639 680 631 688 C 631 688 658 694 658 694 C 658 694 661 670 661 670 C 662 661 665 645 669 637 C 674 629 682 627 686 620 C 682 619 676 617 672 618 C 667 619 664 624 661 628 C 659 630 647 638 645 639 C 640 641 636 632 631 639 C 627 646 636 652 636 658 C 635 664 626 673 624 681 z" id="_cly"/> + <path class="neutral" d="M 560 240 C 560 240 554 258 554 258 C 549 251 550 244 546 242 C 539 240 539 256 533 247 C 533 247 529 255 529 255 C 529 255 522 250 522 250 C 522 264 533 259 542 264 C 546 266 549 271 545 274 C 543 276 541 276 539 277 C 539 277 543 286 543 286 C 531 284 525 274 512 273 C 516 281 528 284 528 291 C 529 295 525 306 522 308 C 517 312 512 314 510 307 C 506 308 506 310 505 314 C 514 319 522 323 529 331 C 537 341 537 361 557 360 C 564 360 568 358 576 358 C 576 358 595 360 595 360 C 595 360 620 359 620 359 C 626 358 638 352 643 347 C 654 338 650 326 649 314 C 649 307 650 300 644 294 C 639 290 634 290 628 290 C 620 290 617 291 611 286 C 609 287 606 289 604 289 C 599 287 601 276 593 278 C 592 279 591 279 590 280 C 589 281 587 282 586 283 C 586 283 585 268 585 268 C 584 268 583 268 582 270 C 579 272 572 290 565 279 C 563 280 558 283 556 283 C 552 282 555 273 556 271 C 559 266 562 265 564 262 C 566 259 566 246 564 243 C 563 241 562 241 560 240 z M 536 763 C 532 771 524 766 517 770 C 512 772 508 777 506 781 C 504 784 502 788 499 789 C 496 790 492 787 489 786 C 489 793 492 793 486 798 C 486 798 486 800 486 800 C 490 803 491 803 489 808 C 495 812 492 813 492 819 C 492 819 514 817 514 817 C 514 817 529 815 529 815 C 529 815 544 811 544 811 C 544 811 559 813 559 813 C 562 813 565 813 568 812 C 572 809 583 797 584 792 C 585 788 585 774 590 764 C 592 758 596 755 601 753 C 605 751 611 750 613 746 C 616 741 612 736 611 731 C 609 726 611 721 609 718 C 606 714 603 716 599 714 C 599 714 594 709 594 709 C 591 708 583 705 580 705 C 572 703 572 707 566 712 C 564 713 561 715 560 717 C 559 720 564 724 558 727 C 548 731 534 717 526 719 C 520 721 525 728 523 733 C 521 738 515 741 514 746 C 513 754 530 762 536 763 z M 845 1233 C 838 1239 834 1238 831 1242 C 825 1249 826 1269 838 1275 C 844 1265 853 1243 845 1233 z M 809 1293 C 806 1304 812 1303 813 1311 C 813 1311 811 1325 811 1325 C 811 1332 812 1339 810 1346 C 809 1350 807 1360 811 1363 C 816 1367 820 1361 825 1360 C 828 1359 830 1360 832 1358 C 834 1355 837 1346 838 1343 C 840 1334 846 1316 846 1307 C 846 1307 846 1298 846 1298 C 845 1297 845 1294 844 1293 C 843 1290 839 1288 836 1288 C 830 1287 823 1293 817 1294 C 814 1294 812 1294 809 1293 z M 647 1310 C 647 1310 659 1317 659 1317 C 666 1320 672 1311 671 1307 C 669 1301 654 1298 647 1310 z M 918 1408 C 915 1410 909 1417 912 1421 C 915 1427 924 1428 930 1430 C 939 1434 951 1443 958 1449 C 963 1453 974 1461 981 1458 C 984 1456 985 1453 985 1450 C 985 1444 985 1443 986 1436 C 986 1436 990 1423 990 1423 C 990 1420 990 1416 988 1414 C 985 1412 977 1410 974 1410 C 963 1410 954 1413 942 1411 C 935 1409 925 1406 918 1408 z M 1477 1489 C 1478 1492 1478 1494 1480 1497 C 1487 1508 1502 1502 1510 1495 C 1510 1495 1517 1489 1517 1489 C 1520 1486 1522 1486 1525 1485 C 1523 1475 1523 1477 1528 1467 C 1522 1469 1520 1471 1514 1473 C 1514 1473 1496 1477 1496 1477 C 1493 1478 1493 1481 1490 1484 C 1487 1487 1481 1488 1477 1489 z M 1302 1505 C 1300 1506 1297 1507 1295 1507 C 1293 1507 1290 1504 1288 1503 C 1288 1503 1273 1499 1273 1499 C 1264 1498 1256 1503 1240 1497 C 1235 1495 1229 1493 1226 1499 C 1225 1500 1225 1502 1225 1504 C 1226 1505 1226 1506 1227 1506 C 1227 1506 1245 1508 1245 1508 C 1252 1510 1258 1514 1265 1514 C 1270 1514 1272 1513 1276 1513 C 1286 1512 1295 1514 1302 1505 z" id="unplayable"/> + <path class="water" d="M 1774 1010 C 1771 1023 1771 1042 1771 1056 C 1771 1059 1771 1064 1773 1066 C 1775 1068 1780 1069 1783 1071 C 1788 1073 1794 1078 1797 1082 C 1805 1092 1802 1098 1815 1112 C 1815 1112 1841 1134 1841 1134 C 1846 1138 1848 1137 1855 1142 C 1855 1142 1876 1161 1876 1161 C 1880 1164 1884 1166 1890 1167 C 1894 1167 1901 1166 1904 1170 C 1905 1172 1905 1174 1903 1175 C 1900 1177 1893 1176 1890 1183 C 1888 1187 1889 1195 1890 1200 C 1891 1209 1891 1210 1891 1219 C 1891 1219 1888 1219 1888 1219 C 1888 1225 1889 1231 1891 1237 C 1900 1263 1918 1270 1943 1275 C 1952 1277 1961 1278 1970 1279 C 1985 1280 2010 1266 2017 1253 C 2021 1247 2021 1243 2021 1237 C 2021 1237 2021 1226 2021 1226 C 2021 1216 2017 1207 2015 1198 C 2014 1193 2013 1185 2009 1180 C 2006 1175 2001 1174 1996 1172 C 1993 1171 1988 1169 1987 1166 C 1986 1164 1988 1162 1988 1160 C 1988 1160 1988 1154 1988 1154 C 1988 1151 1990 1149 1991 1144 C 1983 1146 1976 1152 1973 1160 C 1965 1156 1961 1145 1958 1137 C 1958 1135 1956 1130 1956 1128 C 1958 1122 1964 1128 1969 1127 C 1969 1127 1984 1123 1984 1123 C 1989 1120 1996 1110 1996 1104 C 1995 1096 1985 1096 1977 1091 C 1973 1088 1968 1083 1964 1082 C 1961 1081 1959 1081 1956 1082 C 1947 1087 1950 1100 1950 1108 C 1940 1106 1933 1094 1934 1085 C 1934 1081 1935 1071 1932 1068 C 1927 1065 1918 1073 1910 1071 C 1910 1071 1902 1069 1902 1069 C 1899 1069 1895 1070 1892 1067 C 1889 1065 1889 1061 1886 1057 C 1883 1053 1877 1049 1872 1047 C 1866 1044 1865 1044 1859 1043 C 1857 1043 1854 1042 1853 1040 C 1852 1037 1854 1035 1856 1033 C 1863 1026 1870 1024 1872 1020 C 1875 1015 1871 1010 1870 1005 C 1869 998 1874 991 1880 987 C 1884 984 1887 984 1890 980 C 1896 971 1892 949 1888 939 C 1887 935 1885 930 1881 927 C 1875 924 1867 926 1861 927 C 1851 929 1837 935 1830 942 C 1830 942 1814 964 1814 964 C 1808 971 1801 975 1796 983 C 1791 991 1795 998 1787 1007 C 1785 1009 1782 1012 1779 1011 C 1777 1011 1775 1011 1774 1010 z" id="unplayable_water"/> + <path class="water" d="M 202 175 C 202 175 202 859 202 859 C 202 859 240 849 240 849 C 272 844 323 842 355 846 C 372 849 395 856 410 862 C 416 865 419 866 424 869 C 426 871 429 873 432 872 C 436 871 445 861 448 858 C 448 858 477 829 477 829 C 483 823 489 819 491 811 C 487 809 487 808 488 804 C 483 799 483 797 489 794 C 488 792 485 788 488 786 C 491 782 496 789 500 786 C 500 786 508 774 508 774 C 515 766 523 765 533 765 C 527 762 510 754 512 746 C 513 741 519 738 521 733 C 523 727 517 721 525 718 C 530 716 537 719 542 722 C 549 725 553 726 560 724 C 559 722 557 719 558 717 C 558 714 561 713 563 712 C 568 708 571 703 576 702 C 580 702 591 705 594 707 C 594 707 599 711 599 711 C 603 714 606 712 610 716 C 613 720 612 725 613 729 C 614 736 616 737 617 741 C 617 745 613 750 611 753 C 615 756 618 759 623 761 C 631 764 640 763 648 763 C 645 759 645 759 647 754 C 643 752 642 752 641 748 C 640 748 638 749 637 749 C 632 749 623 744 624 737 C 626 731 633 724 637 719 C 640 713 640 709 640 703 C 634 707 637 713 628 713 C 627 714 627 715 626 716 C 625 716 624 717 622 717 C 618 715 620 708 620 705 C 613 705 611 700 615 695 C 620 689 627 691 633 683 C 633 683 629 683 629 683 C 612 683 633 668 633 658 C 634 652 625 646 629 639 C 633 632 638 633 642 638 C 646 636 656 630 659 627 C 664 621 665 618 673 615 C 673 615 670 614 670 614 C 670 614 672 594 672 594 C 672 594 676 553 676 553 C 676 553 684 485 684 485 C 684 476 684 469 682 460 C 679 450 664 424 658 413 C 658 413 629 359 629 359 C 629 359 620 362 620 362 C 620 362 593 362 593 362 C 593 362 572 360 572 360 C 563 361 553 365 544 359 C 534 353 531 337 526 330 C 522 325 515 322 510 319 C 508 318 504 316 503 313 C 503 312 504 311 504 310 C 507 299 511 311 517 309 C 523 307 523 303 525 298 C 525 298 526 293 526 293 C 527 286 519 284 514 279 C 512 277 509 274 511 272 C 514 270 518 272 520 272 C 527 275 531 280 539 282 C 537 275 539 275 545 273 C 542 260 529 267 522 258 C 518 253 519 244 528 251 C 531 247 532 243 536 248 C 538 246 540 241 543 240 C 550 237 553 249 553 254 C 559 242 556 229 556 216 C 556 216 556 175 556 175 C 556 175 202 175 202 175 z" id="_nat"/> + <path class="water" d="M 560 175 C 560 175 560 228 560 228 C 560 243 569 238 568 255 C 567 263 563 265 560 269 C 558 273 556 278 556 282 C 556 282 568 277 568 277 C 568 277 568 281 568 281 C 577 279 575 273 580 268 C 586 263 587 268 587 273 C 587 273 587 278 587 278 C 589 277 593 276 595 276 C 601 276 602 283 604 287 C 606 286 609 285 611 285 C 613 285 614 287 618 288 C 618 288 630 288 630 288 C 633 288 635 288 638 289 C 653 293 651 308 652 320 C 653 327 654 334 651 341 C 647 348 639 353 632 357 C 632 357 667 423 667 423 C 679 443 688 458 688 482 C 688 482 684 516 684 516 C 684 516 677 587 677 587 C 677 587 674 615 674 615 C 674 615 698 620 698 620 C 701 621 705 623 708 623 C 710 622 718 615 720 612 C 730 604 737 597 748 590 C 775 574 808 568 838 562 C 848 559 868 554 877 555 C 886 543 890 538 905 541 C 901 531 909 531 917 530 C 913 525 913 522 920 519 C 926 517 928 521 932 520 C 935 519 941 513 947 510 C 953 506 957 508 959 506 C 961 504 961 502 964 497 C 967 492 978 481 983 480 C 985 479 987 480 989 480 C 989 466 999 454 1006 442 C 1012 433 1017 421 1023 413 C 1023 413 1042 394 1042 394 C 1042 394 1053 378 1053 378 C 1053 378 1069 356 1069 356 C 1069 356 1080 342 1080 342 C 1082 340 1086 336 1087 335 C 1088 332 1085 330 1085 327 C 1084 324 1088 319 1090 316 C 1094 313 1097 315 1099 312 C 1102 310 1103 305 1109 303 C 1113 301 1120 302 1131 297 C 1140 293 1145 286 1153 281 C 1159 277 1169 272 1172 266 C 1174 263 1172 256 1171 252 C 1171 252 1171 234 1171 234 C 1171 234 1172 219 1172 219 C 1172 219 1174 175 1174 175 C 1174 175 560 175 560 175 z" id="_nrg"/> + <path class="water" d="M 1178 175 C 1178 175 1176 214 1176 214 C 1176 214 1175 230 1175 230 C 1175 230 1175 246 1175 246 C 1175 249 1175 259 1177 261 C 1179 264 1183 263 1184 267 C 1186 271 1183 283 1183 288 C 1189 282 1189 273 1193 266 C 1194 264 1197 260 1199 262 C 1201 265 1199 274 1200 279 C 1204 273 1202 263 1207 258 C 1210 254 1217 257 1218 262 C 1218 267 1215 270 1217 278 C 1222 274 1222 268 1228 266 C 1234 265 1252 272 1249 280 C 1247 286 1236 289 1230 289 C 1232 292 1237 296 1241 297 C 1246 297 1248 293 1260 293 C 1265 285 1277 288 1278 293 C 1278 295 1275 299 1274 301 C 1284 305 1293 302 1303 302 C 1306 302 1312 302 1315 302 C 1322 304 1337 311 1344 314 C 1344 314 1359 319 1359 319 C 1371 322 1386 326 1395 334 C 1398 337 1407 349 1408 353 C 1410 365 1401 379 1393 387 C 1390 390 1387 393 1383 395 C 1379 396 1370 397 1366 397 C 1357 397 1330 394 1322 392 C 1322 392 1313 388 1313 388 C 1309 388 1306 390 1301 388 C 1295 386 1294 383 1287 380 C 1289 391 1296 392 1304 398 C 1308 401 1310 404 1315 406 C 1319 408 1323 409 1327 412 C 1334 419 1331 430 1333 436 C 1335 440 1340 447 1343 451 C 1345 455 1346 459 1350 462 C 1350 462 1369 469 1369 469 C 1372 470 1384 476 1387 475 C 1389 475 1392 472 1394 470 C 1392 465 1389 460 1384 458 C 1381 458 1378 458 1375 457 C 1371 455 1367 450 1365 446 C 1363 441 1364 431 1371 430 C 1375 430 1380 434 1383 436 C 1388 438 1391 439 1397 439 C 1397 439 1423 439 1423 439 C 1430 439 1441 442 1447 441 C 1447 441 1447 439 1447 439 C 1440 437 1430 437 1426 435 C 1420 432 1418 426 1413 422 C 1409 418 1402 415 1401 410 C 1399 404 1408 395 1411 391 C 1420 380 1419 376 1422 372 C 1428 366 1440 366 1448 367 C 1451 360 1452 347 1447 340 C 1443 336 1439 337 1436 331 C 1433 326 1434 317 1430 306 C 1426 297 1421 294 1416 287 C 1428 283 1455 284 1459 299 C 1461 306 1452 309 1450 316 C 1450 318 1450 321 1452 322 C 1456 327 1471 331 1477 329 C 1493 325 1488 312 1489 300 C 1491 290 1496 278 1502 269 C 1504 265 1511 254 1515 253 C 1517 253 1518 253 1520 254 C 1520 249 1518 242 1520 237 C 1526 227 1534 237 1535 244 C 1537 251 1532 257 1532 270 C 1532 285 1533 289 1542 302 C 1543 298 1537 288 1537 282 C 1537 276 1539 270 1541 264 C 1541 260 1542 254 1544 251 C 1544 251 1548 244 1548 244 C 1550 240 1549 236 1554 229 C 1560 222 1565 224 1570 219 C 1574 214 1575 208 1577 204 C 1578 202 1580 199 1583 199 C 1585 199 1587 203 1588 204 C 1591 207 1593 209 1597 210 C 1596 207 1595 203 1595 200 C 1595 194 1598 192 1594 186 C 1593 184 1591 182 1589 181 C 1587 179 1584 177 1582 176 C 1578 174 1566 175 1561 175 C 1561 175 1493 175 1493 175 C 1493 175 1178 175 1178 175 z" id="_bar"/> + <path class="water" d="M 1058 692 C 1058 692 1062 700 1062 700 C 1062 700 1064 709 1064 709 C 1064 709 1085 715 1085 715 C 1085 715 1129 730 1129 730 C 1129 730 1154 733 1154 733 C 1155 731 1156 729 1158 727 C 1167 716 1178 731 1185 738 C 1187 741 1189 744 1192 745 C 1202 747 1204 731 1204 724 C 1204 719 1205 707 1202 703 C 1200 700 1197 701 1192 697 C 1186 692 1184 681 1186 673 C 1188 666 1194 663 1200 660 C 1211 654 1223 653 1236 654 C 1236 654 1250 656 1250 656 C 1264 657 1263 645 1270 640 C 1273 638 1275 638 1278 637 C 1278 637 1286 633 1286 633 C 1289 633 1294 634 1298 634 C 1287 619 1270 633 1268 612 C 1268 612 1261 617 1261 617 C 1261 617 1237 625 1237 625 C 1237 625 1208 636 1208 636 C 1203 638 1185 643 1181 642 C 1176 642 1175 637 1170 635 C 1170 635 1163 633 1163 633 C 1163 633 1154 628 1154 628 C 1154 628 1147 625 1147 625 C 1142 622 1139 614 1140 608 C 1140 599 1144 597 1144 587 C 1144 587 1138 553 1138 553 C 1137 542 1147 531 1154 523 C 1154 523 1166 508 1166 508 C 1166 508 1186 478 1186 478 C 1186 478 1197 464 1197 464 C 1198 462 1198 459 1197 457 C 1194 447 1182 441 1172 440 C 1168 440 1163 441 1159 443 C 1150 449 1142 466 1142 477 C 1142 487 1145 488 1143 494 C 1140 504 1125 518 1117 525 C 1117 525 1103 535 1103 535 C 1100 538 1097 542 1095 545 C 1085 558 1083 563 1079 579 C 1077 590 1074 609 1079 619 C 1082 624 1086 625 1090 629 C 1097 637 1103 646 1100 657 C 1100 661 1097 666 1095 669 C 1085 686 1076 689 1058 692 z" id="_bot"/> + <path class="water" d="M 1060 713 C 1059 721 1057 725 1057 734 C 1057 744 1056 753 1052 763 C 1050 769 1047 776 1041 779 C 1037 781 1031 780 1026 780 C 1021 780 1016 781 1012 785 C 1006 792 1007 803 993 804 C 990 804 985 803 983 804 C 981 805 975 814 973 817 C 966 825 955 830 945 829 C 931 828 931 808 917 804 C 918 812 919 816 924 823 C 927 826 935 831 935 836 C 935 839 932 844 930 846 C 934 846 939 846 942 845 C 946 844 947 842 952 840 C 952 840 967 836 967 836 C 971 834 979 830 984 831 C 985 831 986 831 987 831 C 994 835 986 840 989 844 C 991 848 1002 850 1007 850 C 1024 850 1040 843 1056 836 C 1070 830 1079 824 1094 831 C 1090 839 1098 844 1107 840 C 1113 838 1118 835 1121 830 C 1123 827 1123 824 1125 821 C 1127 818 1130 817 1133 816 C 1135 816 1137 817 1139 816 C 1143 814 1144 805 1144 801 C 1146 790 1140 777 1143 762 C 1143 762 1152 735 1152 735 C 1144 735 1138 735 1130 734 C 1122 732 1115 729 1108 727 C 1097 723 1071 715 1060 713 z" id="_bal"/> + <path class="water" d="M 919 800 C 926 804 928 807 932 813 C 935 817 938 823 943 825 C 950 828 962 823 967 818 C 970 816 973 811 975 808 C 978 805 980 801 981 797 C 982 787 978 782 976 773 C 976 773 965 771 965 771 C 965 771 943 769 943 769 C 943 769 935 769 935 769 C 935 769 926 778 926 778 C 926 778 922 786 922 786 C 922 786 919 800 919 800 z M 966 815 C 961 812 959 809 955 805 C 952 802 945 792 944 788 C 944 785 947 783 949 781 C 953 777 955 772 961 772 C 973 772 972 779 975 788 C 976 792 978 795 976 799 C 975 805 969 810 966 815 z M 957 821 C 954 822 952 823 949 823 C 935 824 939 804 951 812 C 952 813 953 814 954 815 C 956 817 956 819 957 821 z" id="denmark_water"/> + <path class="water" d="M 888 693 C 886 712 885 718 894 735 C 894 735 896 735 896 735 C 902 728 907 729 915 726 C 921 724 926 722 931 718 C 934 717 939 713 942 717 C 944 719 944 721 944 723 C 945 727 944 731 942 735 C 940 739 936 743 936 748 C 937 752 940 754 943 757 C 946 761 945 763 948 765 C 949 766 954 767 956 767 C 956 767 976 771 976 771 C 977 768 980 758 980 756 C 980 751 972 733 970 727 C 970 727 962 698 962 698 C 961 692 958 685 958 679 C 959 674 960 664 954 661 C 950 660 947 665 938 670 C 930 674 928 674 919 679 C 908 686 902 692 888 693 z" id="_ska"/> + <path class="water" d="M 817 848 C 830 846 840 850 849 848 C 856 846 859 840 865 840 C 873 840 877 851 884 847 C 889 844 890 837 890 832 C 890 832 890 819 890 819 C 890 814 889 813 889 807 C 889 801 891 798 891 790 C 891 785 889 784 888 780 C 887 777 887 772 887 769 C 887 769 867 769 867 769 C 853 769 842 778 834 788 C 823 802 821 815 819 832 C 819 832 817 848 817 848 z" id="_hel"/> + <path class="water" d="M 678 650 C 678 650 687 650 687 650 C 693 650 695 650 700 652 C 703 653 705 654 708 655 C 718 662 717 668 711 676 C 704 684 689 696 688 706 C 687 715 695 726 698 734 C 700 741 697 744 697 751 C 697 758 697 764 701 770 C 705 778 709 779 712 789 C 715 797 716 804 707 807 C 710 812 719 820 716 827 C 713 831 710 831 707 837 C 707 837 723 839 723 839 C 726 840 729 841 732 843 C 734 844 735 845 736 847 C 747 862 724 875 714 882 C 712 883 705 887 704 889 C 702 895 714 895 717 900 C 718 902 718 909 724 912 C 726 913 735 911 738 910 C 748 909 771 905 778 899 C 783 894 787 881 790 875 C 796 863 802 857 814 851 C 812 844 814 838 815 831 C 818 807 827 780 851 769 C 862 763 876 765 888 765 C 888 765 892 740 892 740 C 892 737 888 731 886 728 C 883 722 883 718 883 711 C 883 711 883 693 883 693 C 882 691 877 689 875 687 C 870 684 860 676 860 669 C 859 658 875 655 877 645 C 874 644 872 644 869 644 C 867 645 863 646 861 645 C 856 644 857 637 860 634 C 865 630 868 634 872 627 C 872 627 861 623 861 623 C 864 617 867 619 873 617 C 868 608 864 611 863 602 C 863 594 867 577 870 569 C 870 569 874 558 874 558 C 874 558 829 567 829 567 C 803 573 776 579 752 592 C 735 602 727 611 713 623 C 706 629 703 632 695 637 C 689 641 681 643 678 650 z" id="_nth"/> + <path class="water" d="M 498 922 C 498 922 514 932 514 932 C 514 932 541 946 541 946 C 541 946 558 954 558 954 C 558 954 571 952 571 952 C 571 952 583 954 583 954 C 583 954 592 954 592 954 C 592 954 602 962 602 962 C 602 962 610 964 610 964 C 610 964 630 973 630 973 C 627 962 629 965 631 956 C 631 951 627 939 629 936 C 631 933 640 935 643 936 C 646 940 645 945 648 949 C 654 956 668 957 677 957 C 674 951 676 950 682 947 C 691 943 693 947 700 943 C 712 936 714 925 721 915 C 719 913 716 908 714 907 C 712 907 708 908 706 909 C 702 910 698 911 693 911 C 688 911 675 910 671 908 C 666 906 666 904 659 903 C 659 903 630 900 630 900 C 625 899 618 895 613 894 C 606 894 603 902 598 903 C 596 904 593 901 591 900 C 588 899 583 897 579 897 C 574 898 570 901 566 902 C 560 903 559 899 554 900 C 554 900 534 907 534 907 C 534 907 498 922 498 922 z" id="_eng"/> + <path class="water" d="M 490 820 C 490 820 452 859 452 859 C 447 864 439 871 436 877 C 436 877 475 907 475 907 C 479 910 489 918 493 919 C 497 919 511 913 515 911 C 515 911 549 897 549 897 C 554 895 555 892 558 890 C 561 888 563 890 570 886 C 574 884 580 881 583 878 C 585 875 586 872 588 871 C 591 869 592 871 593 873 C 596 871 599 868 602 868 C 604 867 611 870 614 870 C 622 872 625 871 631 867 C 627 866 623 868 619 866 C 613 863 608 854 600 850 C 595 849 584 845 587 838 C 593 825 620 836 618 818 C 617 811 612 813 610 810 C 608 807 617 802 619 801 C 631 793 634 792 645 802 C 647 801 648 800 649 798 C 652 791 648 774 648 766 C 632 770 618 765 607 753 C 603 754 599 755 596 759 C 585 769 591 789 583 799 C 579 803 571 812 567 814 C 561 817 554 814 547 814 C 538 814 535 816 526 817 C 518 819 498 821 490 820 z" id="_iri"/> + <path class="water" d="M 202 1527 C 204 1527 210 1527 212 1526 C 214 1525 216 1518 217 1516 C 219 1509 222 1502 226 1496 C 230 1489 236 1479 242 1473 C 242 1473 254 1464 254 1464 C 260 1458 263 1453 270 1448 C 278 1442 288 1436 298 1434 C 298 1434 336 1430 336 1430 C 353 1425 369 1402 381 1389 C 381 1389 391 1379 391 1379 C 393 1378 396 1376 397 1374 C 401 1366 392 1362 389 1356 C 389 1356 387 1340 387 1340 C 385 1330 379 1321 370 1316 C 361 1311 358 1317 349 1316 C 343 1315 338 1311 333 1308 C 330 1307 325 1305 324 1302 C 323 1298 330 1291 332 1288 C 336 1281 334 1279 336 1273 C 337 1268 340 1265 340 1260 C 340 1252 335 1256 332 1252 C 328 1248 329 1242 331 1238 C 332 1232 335 1224 340 1220 C 344 1216 347 1217 352 1210 C 361 1200 375 1172 378 1159 C 378 1159 383 1139 383 1139 C 385 1134 387 1130 388 1123 C 388 1114 383 1107 390 1099 C 393 1096 397 1093 401 1093 C 404 1093 410 1096 414 1094 C 416 1094 416 1091 418 1090 C 421 1087 427 1086 430 1086 C 441 1087 439 1095 453 1101 C 453 1101 477 1109 477 1109 C 477 1109 503 1123 503 1123 C 503 1123 523 1130 523 1130 C 523 1130 534 1136 534 1136 C 539 1138 544 1138 547 1139 C 555 1141 558 1149 570 1148 C 575 1148 583 1139 587 1135 C 589 1132 598 1113 598 1110 C 599 1107 598 1106 598 1103 C 599 1096 605 1086 610 1082 C 606 1069 610 1069 609 1059 C 607 1048 598 1044 597 1032 C 597 1028 598 1025 597 1021 C 597 1016 594 1014 593 1012 C 592 1009 592 1007 591 1005 C 590 1003 586 1001 584 999 C 579 994 574 988 568 984 C 563 982 554 983 549 976 C 548 974 547 971 547 969 C 554 967 554 964 555 957 C 555 957 535 947 535 947 C 512 936 496 928 475 912 C 453 895 439 879 412 867 C 390 857 356 848 332 848 C 332 848 290 848 290 848 C 290 848 278 849 278 849 C 259 850 241 852 222 858 C 219 859 205 862 203 864 C 202 865 202 869 202 871 C 202 871 202 1527 202 1527 z" id="_mid"/> + <path class="water" d="M 674 1309 C 673 1311 672 1313 671 1315 C 670 1316 668 1318 667 1319 C 658 1325 653 1312 645 1313 C 645 1313 624 1321 624 1321 C 610 1325 596 1324 582 1324 C 577 1324 568 1325 564 1329 C 564 1329 546 1351 546 1351 C 540 1356 526 1353 517 1359 C 510 1363 506 1371 500 1372 C 497 1372 493 1370 490 1369 C 484 1368 482 1371 475 1369 C 475 1369 452 1360 452 1360 C 440 1358 436 1362 427 1363 C 418 1364 418 1358 408 1364 C 407 1365 404 1366 403 1368 C 400 1372 404 1376 406 1379 C 410 1386 412 1399 418 1406 C 424 1412 442 1414 451 1414 C 454 1414 460 1412 462 1413 C 466 1414 464 1418 470 1422 C 477 1426 494 1429 502 1429 C 516 1429 517 1420 526 1417 C 528 1417 537 1417 539 1417 C 543 1419 544 1421 549 1420 C 549 1420 571 1412 571 1412 C 580 1409 591 1407 600 1407 C 621 1407 621 1410 634 1410 C 650 1410 670 1409 685 1413 C 691 1415 694 1417 699 1421 C 701 1423 705 1427 708 1427 C 711 1428 714 1425 717 1425 C 717 1425 739 1419 739 1419 C 744 1420 753 1424 757 1425 C 762 1426 763 1422 770 1422 C 780 1423 784 1427 791 1429 C 799 1430 807 1428 815 1427 C 815 1427 819 1396 819 1396 C 819 1389 816 1376 813 1370 C 811 1366 808 1363 807 1358 C 805 1353 809 1345 809 1338 C 809 1335 808 1325 807 1324 C 806 1322 801 1320 799 1320 C 799 1320 779 1313 779 1313 C 758 1306 746 1305 724 1305 C 724 1305 692 1308 692 1308 C 686 1309 680 1310 674 1309 z" id="_wes"/> + <path class="water" d="M 674 1307 C 674 1307 698 1304 698 1304 C 720 1301 738 1299 760 1304 C 760 1304 809 1319 809 1319 C 813 1307 808 1308 806 1300 C 806 1296 807 1293 808 1290 C 821 1296 822 1286 836 1285 C 836 1269 826 1273 825 1257 C 825 1251 826 1242 831 1239 C 835 1236 838 1236 843 1232 C 848 1227 848 1222 854 1221 C 860 1219 870 1222 876 1223 C 876 1223 874 1210 874 1210 C 874 1206 874 1202 873 1198 C 872 1190 868 1189 862 1184 C 855 1179 852 1175 842 1177 C 832 1179 829 1185 822 1189 C 822 1189 799 1197 799 1197 C 790 1201 784 1208 774 1210 C 765 1212 758 1207 752 1200 C 750 1197 750 1195 748 1193 C 743 1190 739 1192 734 1191 C 728 1190 721 1184 715 1183 C 711 1183 706 1185 702 1186 C 682 1196 698 1217 680 1232 C 672 1238 656 1244 646 1246 C 640 1247 622 1249 618 1251 C 614 1254 616 1257 613 1259 C 611 1260 608 1260 605 1262 C 605 1262 600 1267 600 1267 C 600 1267 594 1273 594 1273 C 590 1276 586 1284 583 1289 C 583 1289 576 1298 576 1298 C 573 1304 576 1316 582 1319 C 584 1320 588 1320 590 1320 C 590 1320 606 1320 606 1320 C 619 1320 631 1316 642 1310 C 647 1307 648 1304 653 1301 C 661 1298 669 1301 674 1307 z" id="_gol"/> + <path class="water" d="M 816 1367 C 820 1377 822 1381 822 1392 C 822 1392 822 1404 822 1404 C 822 1404 819 1426 819 1426 C 829 1424 844 1425 849 1435 C 850 1438 850 1442 850 1445 C 856 1444 870 1435 876 1432 C 876 1432 906 1419 906 1419 C 911 1415 913 1407 919 1405 C 924 1403 938 1408 944 1409 C 944 1409 951 1409 951 1409 C 964 1409 970 1407 983 1409 C 992 1398 994 1403 1004 1406 C 1006 1403 1014 1390 1015 1387 C 1016 1382 1014 1376 1012 1371 C 1010 1367 1009 1360 1006 1357 C 1003 1352 996 1347 991 1342 C 989 1340 987 1338 986 1335 C 985 1333 985 1330 982 1328 C 980 1325 970 1326 966 1326 C 967 1319 963 1315 960 1309 C 958 1306 957 1302 954 1300 C 950 1298 942 1298 938 1297 C 932 1296 927 1292 923 1287 C 923 1287 913 1273 913 1273 C 913 1273 898 1255 898 1255 C 898 1255 885 1238 885 1238 C 883 1236 880 1229 878 1228 C 875 1226 869 1225 866 1225 C 862 1224 857 1222 853 1225 C 847 1229 850 1237 850 1243 C 850 1252 849 1258 845 1266 C 843 1270 840 1275 839 1279 C 838 1288 844 1287 847 1293 C 848 1297 848 1303 848 1307 C 848 1318 842 1335 839 1346 C 838 1350 836 1358 833 1360 C 830 1363 828 1361 825 1362 C 823 1363 818 1366 816 1367 z" id="_tyn"/> + <path class="water" d="M 1104 1335 C 1104 1321 1105 1317 1108 1304 C 1109 1300 1111 1292 1109 1288 C 1107 1284 1101 1280 1098 1277 C 1085 1265 1083 1264 1069 1254 C 1069 1254 1040 1231 1040 1231 C 1040 1231 1022 1219 1022 1219 C 1022 1219 1006 1205 1006 1205 C 1001 1201 995 1197 990 1192 C 986 1187 981 1178 982 1172 C 983 1164 989 1163 985 1155 C 978 1159 976 1173 969 1172 C 966 1171 962 1165 962 1162 C 960 1155 966 1148 964 1145 C 962 1142 958 1143 956 1144 C 952 1144 943 1148 939 1150 C 937 1151 935 1153 934 1155 C 933 1158 936 1161 935 1164 C 935 1166 932 1169 931 1171 C 929 1175 930 1182 931 1186 C 937 1198 952 1206 960 1219 C 964 1227 964 1234 967 1242 C 970 1251 979 1261 986 1267 C 991 1271 997 1274 1004 1276 C 1007 1276 1016 1277 1018 1279 C 1022 1283 1016 1286 1022 1293 C 1029 1301 1059 1319 1070 1327 C 1073 1329 1076 1332 1079 1335 C 1080 1337 1082 1340 1084 1340 C 1087 1341 1100 1336 1104 1335 z" id="_adr"/> + <path class="water" d="M 1044 1331 C 1040 1336 1036 1342 1032 1348 C 1031 1350 1029 1353 1030 1356 C 1031 1359 1036 1361 1038 1363 C 1044 1369 1047 1377 1043 1385 C 1039 1391 1035 1386 1031 1390 C 1029 1392 1029 1397 1028 1400 C 1025 1406 1020 1416 1015 1419 C 1007 1425 999 1419 1001 1409 C 995 1406 990 1403 987 1411 C 990 1413 992 1414 993 1418 C 993 1422 990 1429 989 1434 C 989 1434 986 1455 986 1455 C 985 1458 982 1460 979 1461 C 969 1462 961 1453 954 1448 C 950 1444 937 1436 932 1433 C 932 1433 919 1429 919 1429 C 914 1427 912 1423 909 1423 C 906 1422 901 1425 898 1426 C 898 1426 874 1437 874 1437 C 877 1455 856 1455 855 1470 C 854 1481 866 1486 871 1495 C 876 1506 872 1516 868 1527 C 868 1527 1223 1527 1223 1527 C 1223 1527 1224 1509 1224 1509 C 1224 1509 1211 1479 1211 1479 C 1209 1474 1206 1464 1199 1464 C 1195 1464 1196 1471 1189 1475 C 1187 1467 1186 1458 1178 1454 C 1177 1456 1174 1462 1171 1461 C 1170 1460 1169 1457 1168 1455 C 1167 1450 1167 1444 1165 1439 C 1162 1431 1155 1427 1154 1421 C 1153 1412 1162 1412 1165 1411 C 1173 1409 1173 1407 1182 1408 C 1193 1410 1195 1414 1203 1416 C 1204 1408 1203 1409 1195 1405 C 1193 1404 1191 1403 1188 1402 C 1188 1402 1164 1406 1164 1406 C 1161 1406 1157 1407 1155 1406 C 1153 1405 1136 1392 1151 1387 C 1146 1384 1145 1387 1141 1385 C 1138 1384 1133 1379 1131 1376 C 1125 1369 1128 1367 1126 1361 C 1124 1355 1111 1342 1106 1340 C 1102 1338 1088 1343 1084 1346 C 1081 1348 1080 1351 1077 1351 C 1074 1352 1070 1349 1068 1348 C 1059 1344 1052 1337 1044 1331 z" id="_ion"/> + <path class="water" d="M 1227 1321 C 1227 1321 1242 1339 1242 1339 C 1237 1336 1231 1334 1225 1333 C 1225 1333 1233 1347 1233 1347 C 1233 1347 1217 1339 1217 1339 C 1217 1339 1221 1347 1221 1347 C 1214 1346 1202 1339 1201 1332 C 1200 1330 1201 1329 1201 1327 C 1184 1333 1203 1353 1209 1360 C 1211 1362 1214 1366 1214 1369 C 1213 1373 1207 1369 1204 1370 C 1202 1370 1201 1371 1199 1372 C 1200 1374 1202 1377 1202 1379 C 1202 1381 1200 1383 1200 1386 C 1202 1390 1213 1394 1217 1395 C 1214 1391 1213 1391 1210 1388 C 1204 1382 1209 1379 1213 1380 C 1215 1381 1219 1384 1221 1385 C 1227 1389 1231 1389 1234 1392 C 1238 1395 1238 1401 1233 1404 C 1233 1404 1233 1413 1233 1413 C 1233 1413 1234 1425 1234 1425 C 1229 1423 1225 1418 1219 1418 C 1217 1418 1213 1418 1213 1421 C 1213 1425 1224 1429 1218 1434 C 1213 1439 1210 1431 1200 1434 C 1200 1434 1209 1455 1209 1455 C 1209 1455 1212 1470 1212 1470 C 1212 1470 1223 1497 1223 1497 C 1225 1496 1228 1493 1231 1493 C 1234 1492 1239 1494 1242 1496 C 1247 1497 1252 1498 1257 1498 C 1267 1498 1268 1496 1279 1498 C 1279 1498 1296 1504 1296 1504 C 1298 1504 1300 1503 1302 1502 C 1305 1502 1306 1502 1309 1501 C 1318 1498 1333 1485 1339 1478 C 1345 1470 1350 1464 1350 1454 C 1340 1451 1353 1448 1352 1438 C 1352 1438 1335 1442 1335 1442 C 1333 1442 1330 1442 1329 1440 C 1328 1436 1332 1436 1329 1429 C 1328 1429 1326 1430 1325 1430 C 1316 1429 1322 1415 1318 1410 C 1314 1404 1302 1405 1305 1399 C 1306 1397 1309 1395 1309 1393 C 1310 1390 1308 1387 1309 1384 C 1309 1381 1311 1379 1311 1376 C 1311 1374 1308 1369 1306 1366 C 1305 1362 1304 1357 1303 1353 C 1297 1354 1293 1358 1289 1357 C 1286 1357 1285 1354 1284 1352 C 1281 1347 1276 1342 1280 1336 C 1284 1328 1294 1326 1300 1319 C 1300 1319 1288 1320 1288 1320 C 1288 1320 1280 1315 1280 1315 C 1280 1315 1259 1313 1259 1313 C 1259 1313 1238 1316 1238 1316 C 1238 1316 1227 1321 1227 1321 z" id="_aeg"/> + <path class="water" d="M 1304 1506 C 1296 1516 1288 1514 1277 1515 C 1272 1516 1268 1518 1263 1516 C 1258 1515 1255 1513 1245 1511 C 1245 1511 1227 1509 1227 1509 C 1227 1509 1227 1527 1227 1527 C 1227 1527 1582 1527 1582 1527 C 1582 1527 1582 1515 1582 1515 C 1582 1515 1579 1485 1579 1485 C 1579 1485 1576 1461 1576 1461 C 1576 1461 1571 1462 1571 1462 C 1571 1462 1571 1440 1571 1440 C 1571 1440 1567 1432 1567 1432 C 1567 1432 1572 1420 1572 1420 C 1572 1420 1572 1409 1572 1409 C 1564 1413 1565 1422 1558 1424 C 1551 1427 1543 1417 1534 1423 C 1530 1426 1524 1436 1519 1442 C 1512 1449 1495 1455 1485 1454 C 1478 1454 1473 1450 1467 1447 C 1457 1441 1448 1435 1436 1435 C 1434 1435 1431 1435 1430 1435 C 1419 1438 1424 1450 1419 1457 C 1417 1460 1407 1463 1403 1464 C 1397 1465 1390 1463 1385 1459 C 1385 1459 1378 1451 1378 1451 C 1376 1450 1371 1450 1368 1449 C 1365 1449 1362 1447 1359 1448 C 1353 1450 1355 1458 1350 1469 C 1342 1482 1332 1492 1319 1500 C 1314 1502 1310 1506 1304 1506 z M 1536 1460 C 1534 1467 1530 1468 1527 1474 C 1525 1479 1528 1482 1526 1485 C 1525 1487 1523 1487 1520 1490 C 1515 1493 1513 1498 1504 1502 C 1495 1507 1483 1508 1477 1498 C 1476 1496 1475 1493 1475 1491 C 1474 1485 1481 1487 1487 1482 C 1491 1479 1490 1477 1494 1475 C 1494 1475 1517 1470 1517 1470 C 1524 1467 1528 1462 1536 1460 z" id="_eas"/> + <path class="water" d="M 1348 1283 C 1350 1286 1356 1290 1357 1292 C 1359 1297 1352 1298 1349 1299 C 1334 1302 1327 1302 1314 1313 C 1314 1313 1297 1329 1297 1329 C 1294 1332 1292 1335 1287 1333 C 1282 1339 1281 1340 1285 1347 C 1291 1341 1304 1327 1311 1323 C 1318 1320 1324 1326 1335 1323 C 1341 1321 1342 1318 1347 1316 C 1347 1316 1357 1312 1357 1312 C 1357 1312 1366 1308 1366 1308 C 1366 1308 1375 1305 1375 1305 C 1371 1303 1366 1304 1363 1301 C 1359 1297 1363 1291 1371 1290 C 1366 1279 1358 1276 1348 1283 z" id="constantinople_water"/> + <path class="water" d="M 1570 1032 C 1570 1032 1546 1047 1546 1047 C 1546 1047 1511 1064 1511 1064 C 1511 1064 1483 1087 1483 1087 C 1483 1087 1462 1097 1462 1097 C 1462 1097 1474 1104 1474 1104 C 1481 1109 1488 1117 1496 1118 C 1505 1119 1513 1102 1521 1115 C 1523 1119 1523 1123 1520 1126 C 1515 1130 1507 1128 1503 1130 C 1500 1132 1499 1135 1496 1137 C 1492 1140 1489 1140 1486 1142 C 1483 1144 1478 1151 1475 1154 C 1471 1158 1467 1161 1461 1159 C 1457 1156 1454 1153 1453 1148 C 1453 1145 1454 1140 1452 1137 C 1449 1133 1442 1133 1438 1131 C 1435 1130 1432 1128 1432 1125 C 1433 1120 1438 1115 1442 1112 C 1444 1110 1448 1109 1449 1106 C 1450 1103 1448 1101 1445 1101 C 1439 1101 1428 1108 1417 1105 C 1408 1102 1409 1097 1402 1094 C 1405 1089 1409 1089 1415 1088 C 1413 1087 1413 1086 1411 1085 C 1404 1084 1389 1089 1384 1093 C 1380 1097 1378 1102 1375 1104 C 1372 1105 1369 1103 1366 1102 C 1368 1105 1371 1109 1371 1112 C 1371 1116 1368 1120 1366 1123 C 1364 1127 1362 1133 1361 1138 C 1359 1145 1360 1152 1357 1158 C 1353 1166 1347 1166 1344 1176 C 1344 1176 1339 1210 1339 1210 C 1336 1216 1332 1214 1329 1218 C 1325 1223 1327 1230 1325 1235 C 1325 1235 1322 1246 1322 1246 C 1322 1250 1325 1253 1327 1256 C 1327 1256 1337 1273 1337 1273 C 1338 1275 1340 1279 1343 1280 C 1347 1281 1350 1277 1355 1275 C 1366 1273 1370 1281 1375 1289 C 1375 1289 1400 1284 1400 1284 C 1406 1283 1414 1283 1419 1280 C 1419 1280 1442 1257 1442 1257 C 1453 1248 1463 1244 1477 1241 C 1477 1241 1501 1237 1501 1237 C 1506 1236 1507 1233 1511 1233 C 1519 1232 1522 1247 1529 1244 C 1530 1243 1532 1242 1533 1241 C 1534 1240 1536 1238 1537 1237 C 1537 1237 1552 1247 1552 1247 C 1552 1247 1553 1242 1553 1242 C 1561 1246 1570 1250 1579 1251 C 1582 1251 1584 1250 1587 1250 C 1603 1249 1607 1247 1622 1242 C 1640 1236 1637 1241 1655 1229 C 1670 1218 1687 1202 1673 1183 C 1670 1179 1665 1173 1660 1171 C 1652 1167 1640 1168 1630 1164 C 1620 1159 1611 1151 1601 1145 C 1592 1140 1583 1136 1573 1133 C 1573 1133 1556 1129 1556 1129 C 1556 1129 1543 1124 1543 1124 C 1538 1123 1533 1122 1530 1117 C 1528 1115 1527 1111 1530 1109 C 1534 1108 1539 1112 1543 1110 C 1547 1108 1546 1102 1547 1098 C 1549 1088 1555 1079 1556 1069 C 1554 1069 1551 1069 1549 1069 C 1544 1068 1540 1063 1544 1058 C 1544 1058 1565 1042 1565 1042 C 1568 1040 1571 1037 1570 1032 z" id="_bla"/> + </g> + + <!-- drawing layer: supply centers --> + <g id="SupplyCenterLayer"/> + + <!-- drawing layer: orders --> + <g id="OrderLayer"> + <g id="Layer2"/> + <g id="Layer1"/> + </g> + + <!-- drawing layer: units --> + <g id="UnitLayer"/> + + <!-- drawing layer: dislodged units --> + <g id="DislodgedUnitLayer"/> + + <!-- above-the-unit drawing layer --> + <g id="HighestOrderLayer"/> + + <!-- Location abbreviations --> + <g class="labeltext24" id="BriefLabelLayer"> + <text x="649.4" y="924.2">SWI</text> + <text class="labeltext18" x="849.4" y="1100.6">ADR</text> + <text x="1085.7" y="1296.4">AEG</text> + <text class="labeltext18" x="932.4" y="1164.4">ALB</text> + <text x="1368.8" y="1115.5">ANK</text> + <text class="labeltext18" x="832.3" y="1151.7">APU</text> + <text x="1603" y="1145.3">ARM</text> + <text x="827.2" y="660.2">BAL</text> + <text x="1077.8" y="59.3">BAR</text> + <text class="labeltext18" x="591.8" y="796.2">BEL</text> + <text class="labeltext18" x="760" y="728">BER</text> + <text x="1319.8" y="1034.6">BLA</text> + <text class="labeltext18" x="800.4" y="855.8">BOH</text> + <text class="labeltext18" x="430" y="866.4">BRE</text> + <text class="labeltext18" x="953.7" y="964.3">BUD</text> + <text class="labeltext18" x="1021.8" y="1109.1">BUL</text> + <text class="labeltext18" x="530.1" y="915.4">BUR</text> + <text class="labeltext18" x="457.7" y="479">CLY</text> + <text x="1202.8" y="1177.2">CON</text> + <text x="718.2" y="584.3">DEN</text> + <text x="1164.4" y="1343.3">EAS</text> + <text class="labeltext18" x="496" y="500.3">EDI</text> + <text x="466.2" y="762.6">ENG</text> + <text x="1030.3" y="347">FIN</text> + <text class="labeltext18" x="940.9" y="840.9">GAL</text> + <text class="labeltext18" x="447" y="972.8">GAS</text> + <text class="labeltext18" x="981.4" y="1160.2">GRE</text> + <text x="929.6" y="538.9">BOT</text> + <text x="525.8" y="1107">LYO</text> + <text x="654.2" y="665.4">HEL</text> + <text class="labeltext18" x="608.8" y="749.3">HOL</text> + <text x="894.1" y="1251.7">ION</text> + <text x="318" y="711.4">IRI</text> + <text class="labeltext18" x="691.9" y="753.6">KIE</text> + <text class="labeltext18" x="500.3" y="734.4">LON</text> + <text x="1030.3" y="640.8">LVN</text> + <text class="labeltext18" x="449.2" y="564.1">LVP</text> + <text class="labeltext18" x="572.6" y="962.2">MAR</text> + <text x="153.3" y="815.3">MAO</text> + <text x="1568.9" y="572.6">MOS</text> + <text class="labeltext18" x="681.2" y="872.8">MUN</text> + <text x="240.6" y="1317.7">NAF</text> + <text class="labeltext18" x="832.3" y="1211.3">NAP</text> + <text x="191.6" y="268.2">NAO</text> + <text x="595.5" y="512">NTH</text> + <text x="760" y="391.7">NWY</text> + <text x="664.2" y="161.8">NWG</text> + <text class="labeltext18" x="481.1" y="892">PAR</text> + <text class="labeltext18" x="489.6" y="802.5">PIC</text> + <text class="labeltext18" x="628" y="1004.8">PIE</text> + <text transform="translate(175.1 1102.1) rotate(-60)" x="0" y="0">POR</text> + <text class="labeltext18" x="938.8" y="687.6">PRU</text> + <text class="labeltext18" transform="translate(729 1092) rotate(45)" x="0" y="0">ROM</text> + <text class="labeltext18" x="632.2" y="826">RUH</text> + <text class="labeltext18" x="1036.7" y="1015.4">RUM</text> + <text class="labeltext18" x="964.3" y="1100.6">SER</text> + <text x="1479.3" y="937.7">SEV</text> + <text class="labeltext18" x="791.9" y="779.1">SIL</text> + <text transform="translate(754.5 571.2) rotate(75)" x="0" y="0">SKA</text> + <text x="1194.2" y="1247.5">SMY</text> + <text x="302.3" y="1119.7">SPA</text> + <text x="1473.1" y="217.1">STP</text> + <text x="845.1" y="391.7">SWE</text> + <text x="1639.2" y="1292.2">SYR</text> + <text class="labeltext18" x="826" y="966.5">TRI</text> + <text x="634.4" y="1351.8">TUN</text> + <text class="labeltext18" x="706.5" y="1069">TUS</text> + <text class="labeltext18" x="717.4" y="936.7">TYR</text> + <text x="710" y="1209.1">TYS</text> + <text x="1119.7" y="860">UKR</text> + <text class="labeltext18" x="749.3" y="962.2">VEN</text> + <text class="labeltext18" x="834.5" y="919.6">VIE</text> + <text class="labeltext18" x="436.4" y="719.5">WAL</text> + <text class="labeltext18" x="938.8" y="783.4">WAR</text> + <text x="400.2" y="1209.1">WES</text> + <text class="labeltext18" x="496" y="653.5">YOR</text> + </g> + + <!-- Current phase --> + <rect x="25" y="25" height="70" width="750" class="currentnoterect" /> + <text x="35" y="50" class="currentnotetext" id="CurrentNote"> </text> + <text x="35" y="85" class="currentnotetext" id="CurrentNote2"> </text> + <text x="1650" y="1325" class="currentphasetext" id="CurrentPhase">S1901M</text> + + <g id="MouseLayer" class="invisible" transform="translate(-195 -170)" > + <g id="con"> + <path d="M 1331 1267 C 1320 1272 1310 1264 1298 1274 C 1289 1282 1298 1291 1291 1305 C 1289 1310 1284 1314 1287 1316 C 1289 1319 1295 1316 1303 1317 C 1303 1317 1303 1319 1303 1319 C 1299 1321 1292 1327 1290 1332 C 1294 1330 1296 1329 1297 1324 C 1297 1324 1323 1304 1323 1304 C 1323 1304 1337 1298 1337 1298 C 1337 1298 1356 1294 1356 1294 C 1356 1294 1340 1282 1340 1282 C 1340 1282 1331 1267 1331 1267 z M 1414 1284 C 1414 1284 1389 1288 1389 1288 C 1389 1288 1375 1292 1375 1292 C 1372 1292 1361 1293 1364 1299 C 1366 1301 1376 1303 1380 1304 C 1380 1304 1380 1306 1380 1306 C 1380 1306 1367 1310 1367 1310 C 1367 1310 1357 1315 1357 1315 C 1357 1315 1348 1318 1348 1318 C 1341 1321 1342 1326 1329 1326 C 1322 1326 1314 1323 1308 1328 C 1304 1331 1290 1344 1288 1349 C 1287 1351 1287 1353 1288 1356 C 1296 1354 1305 1345 1307 1359 C 1307 1359 1307 1364 1307 1364 C 1311 1361 1315 1355 1319 1355 C 1323 1354 1328 1358 1331 1360 C 1335 1363 1340 1364 1345 1365 C 1355 1366 1363 1362 1368 1361 C 1374 1361 1378 1364 1383 1365 C 1389 1365 1393 1361 1400 1361 C 1405 1361 1407 1361 1412 1362 C 1414 1363 1418 1364 1419 1363 C 1422 1362 1424 1355 1424 1352 C 1429 1337 1427 1328 1422 1314 C 1422 1314 1416 1297 1416 1297 C 1415 1292 1416 1288 1414 1284 z M 1346 1295 C 1346 1295 1346 1296 1346 1296 C 1346 1296 1345 1295 1345 1295 C 1345 1295 1346 1295 1346 1295 z"/> + <path d="M 1348 1283 C 1350 1286 1356 1290 1357 1292 C 1359 1297 1352 1298 1349 1299 C 1334 1302 1327 1302 1314 1313 C 1314 1313 1297 1329 1297 1329 C 1294 1332 1292 1335 1287 1333 C 1282 1339 1281 1340 1285 1347 C 1291 1341 1304 1327 1311 1323 C 1318 1320 1324 1326 1335 1323 C 1341 1321 1342 1318 1347 1316 C 1347 1316 1357 1312 1357 1312 C 1357 1312 1366 1308 1366 1308 C 1366 1308 1375 1305 1375 1305 C 1371 1303 1366 1304 1363 1301 C 1359 1297 1363 1291 1371 1290 C 1366 1279 1358 1276 1348 1283 z"/> + </g> + <g id="den"> + <path d="M 939 716 C 933 720 923 725 917 728 C 912 729 904 731 900 734 C 896 737 895 742 894 746 C 892 754 888 769 889 777 C 891 785 894 784 894 796 C 899 796 912 796 916 798 C 919 794 919 789 920 784 C 922 779 927 774 930 770 C 932 768 934 767 936 766 C 938 766 942 768 943 763 C 943 757 933 755 934 747 C 934 743 938 738 940 734 C 943 728 943 721 939 716 z M 946 786 C 951 798 951 796 959 805 C 962 809 962 810 967 813 C 967 813 969 804 969 804 C 971 802 974 800 974 797 C 975 795 973 791 972 788 C 970 782 972 777 964 775 C 963 775 959 775 958 775 C 954 776 955 780 946 786 z M 944 812 C 943 813 943 815 943 816 C 942 826 959 823 952 815 C 951 815 950 814 949 814 C 947 813 946 812 944 812 z"/> + <path d="M 919 800 C 926 804 928 807 932 813 C 935 817 938 823 943 825 C 950 828 962 823 967 818 C 970 816 973 811 975 808 C 978 805 980 801 981 797 C 982 787 978 782 976 773 C 976 773 965 771 965 771 C 965 771 943 769 943 769 C 943 769 935 769 935 769 C 935 769 926 778 926 778 C 926 778 922 786 922 786 C 922 786 919 800 919 800 z M 966 815 C 961 812 959 809 955 805 C 952 802 945 792 944 788 C 944 785 947 783 949 781 C 953 777 955 772 961 772 C 973 772 972 779 975 788 C 976 792 978 795 976 799 C 975 805 969 810 966 815 z M 957 821 C 954 822 952 823 949 823 C 935 824 939 804 951 812 C 952 813 953 814 954 815 C 956 817 956 819 957 821 z"/> + </g> + <path id="ank" d="M 1424 1364 C 1437 1361 1448 1353 1459 1346 C 1464 1343 1470 1337 1475 1336 C 1482 1334 1492 1338 1499 1340 C 1510 1342 1518 1341 1528 1336 C 1544 1328 1555 1307 1575 1297 C 1587 1291 1598 1293 1611 1293 C 1614 1293 1621 1292 1624 1292 C 1646 1286 1638 1257 1637 1241 C 1618 1244 1604 1253 1583 1253 C 1566 1253 1565 1248 1554 1246 C 1553 1247 1553 1248 1551 1248 C 1548 1249 1541 1242 1538 1240 C 1535 1242 1529 1247 1526 1246 C 1521 1245 1517 1235 1511 1235 C 1507 1236 1507 1239 1497 1241 C 1483 1243 1471 1243 1457 1249 C 1450 1253 1440 1261 1435 1266 C 1433 1268 1421 1282 1420 1284 C 1419 1286 1419 1290 1419 1292 C 1419 1300 1423 1305 1425 1312 C 1428 1318 1430 1326 1431 1333 C 1432 1342 1427 1355 1424 1364 z"/> + <path id="arm" d="M 1671 1218 C 1663 1226 1648 1237 1638 1240 C 1638 1240 1641 1241 1641 1241 C 1641 1241 1643 1272 1643 1272 C 1642 1279 1638 1298 1639 1303 C 1641 1311 1648 1312 1652 1318 C 1658 1325 1657 1329 1657 1337 C 1657 1337 1708 1337 1708 1337 C 1708 1337 1720 1338 1720 1338 C 1720 1338 1730 1338 1730 1338 C 1730 1338 1825 1348 1825 1348 C 1825 1348 1837 1349 1837 1349 C 1837 1349 1845 1349 1845 1349 C 1853 1349 1867 1347 1874 1345 C 1892 1339 1913 1320 1927 1307 C 1927 1307 1956 1280 1956 1280 C 1956 1280 1938 1277 1938 1277 C 1922 1273 1905 1266 1896 1251 C 1894 1251 1890 1252 1888 1252 C 1883 1251 1872 1247 1869 1242 C 1868 1240 1867 1220 1858 1221 C 1853 1221 1846 1232 1843 1236 C 1833 1248 1822 1262 1805 1261 C 1784 1260 1786 1252 1767 1252 C 1767 1252 1753 1252 1753 1252 C 1753 1252 1736 1251 1736 1251 C 1727 1251 1722 1253 1713 1250 C 1697 1243 1680 1223 1671 1218 z"/> + <path id="mos" d="M 2022 319 C 2022 319 1994 345 1994 345 C 1994 345 1970 368 1970 368 C 1929 405 1867 454 1820 484 C 1791 502 1771 510 1741 525 C 1741 525 1716 539 1716 539 C 1708 544 1703 547 1694 551 C 1679 557 1664 558 1648 562 C 1618 570 1610 579 1590 585 C 1590 585 1546 593 1546 593 C 1535 596 1530 601 1521 607 C 1512 612 1496 620 1487 623 C 1468 629 1453 625 1434 632 C 1418 639 1408 654 1405 671 C 1404 678 1406 690 1401 696 C 1391 707 1376 685 1365 691 C 1360 693 1356 701 1353 706 C 1348 713 1340 724 1334 730 C 1321 742 1301 741 1284 741 C 1284 754 1279 767 1278 772 C 1278 777 1279 780 1280 784 C 1280 792 1276 793 1276 804 C 1276 804 1278 828 1278 828 C 1278 838 1278 846 1268 851 C 1264 853 1259 854 1255 856 C 1255 856 1242 870 1242 870 C 1242 870 1228 882 1228 882 C 1226 883 1222 886 1221 888 C 1220 891 1222 897 1223 900 C 1225 911 1226 909 1229 918 C 1229 918 1234 935 1234 935 C 1237 932 1238 930 1242 927 C 1247 925 1271 918 1278 916 C 1278 916 1353 897 1353 897 C 1364 894 1385 889 1396 889 C 1401 889 1407 889 1412 890 C 1415 890 1424 892 1427 892 C 1432 891 1438 885 1448 884 C 1448 884 1481 888 1481 888 C 1490 888 1497 888 1502 880 C 1506 873 1508 855 1519 855 C 1525 855 1535 862 1541 865 C 1551 869 1556 870 1567 870 C 1579 870 1580 865 1588 866 C 1593 867 1602 869 1606 872 C 1617 879 1623 900 1636 913 C 1647 924 1652 919 1660 927 C 1667 936 1662 948 1673 958 C 1678 962 1688 966 1694 968 C 1707 974 1719 977 1732 984 C 1748 993 1759 1009 1770 1017 C 1771 1015 1771 1011 1773 1010 C 1776 1008 1778 1010 1780 1010 C 1784 1009 1786 1005 1788 1002 C 1791 996 1789 991 1793 985 C 1797 977 1808 967 1815 960 C 1815 960 1828 941 1828 941 C 1834 935 1842 931 1849 928 C 1863 923 1881 917 1889 934 C 1892 941 1895 955 1895 963 C 1895 969 1896 975 1892 980 C 1887 988 1878 987 1874 997 C 1869 1008 1879 1013 1874 1020 C 1869 1028 1858 1029 1854 1040 C 1866 1041 1876 1045 1885 1053 C 1892 1060 1891 1065 1895 1067 C 1897 1067 1900 1067 1902 1067 C 1907 1067 1909 1070 1916 1068 C 1920 1068 1926 1064 1930 1065 C 1936 1066 1936 1072 1936 1077 C 1936 1088 1935 1099 1947 1105 C 1947 1096 1946 1088 1954 1081 C 1957 1079 1960 1079 1964 1080 C 1969 1081 1975 1087 1980 1090 C 1985 1093 1994 1094 1997 1100 C 1999 1105 1996 1111 1994 1115 C 1987 1126 1985 1124 1975 1127 C 1973 1128 1969 1130 1967 1130 C 1965 1130 1961 1128 1959 1127 C 1959 1138 1964 1147 1970 1156 C 1976 1153 1983 1143 1989 1143 C 1991 1142 1993 1143 1993 1145 C 1993 1148 1990 1151 1990 1154 C 1989 1160 1993 1158 1988 1166 C 1988 1166 2007 1175 2007 1175 C 2007 1175 2013 1183 2013 1183 C 2013 1183 2022 1217 2022 1217 C 2022 1217 2023 1195 2023 1195 C 2023 1195 2023 1149 2023 1149 C 2023 1149 2023 990 2023 990 C 2023 990 2023 516 2023 516 C 2023 516 2023 378 2023 378 C 2023 378 2023 338 2023 338 C 2023 338 2022 319 2022 319 z"/> + <path id="sev" d="M 1364 1100 C 1367 1100 1372 1102 1374 1101 C 1376 1101 1381 1094 1383 1092 C 1389 1087 1394 1085 1401 1084 C 1404 1083 1410 1082 1413 1084 C 1419 1086 1415 1090 1411 1091 C 1411 1091 1406 1092 1406 1092 C 1408 1094 1410 1099 1412 1100 C 1422 1108 1442 1099 1446 1098 C 1450 1098 1453 1101 1452 1105 C 1452 1110 1446 1112 1442 1115 C 1437 1118 1435 1122 1434 1128 C 1439 1129 1450 1131 1453 1135 C 1457 1140 1451 1155 1464 1157 C 1466 1157 1467 1157 1469 1156 C 1474 1154 1480 1144 1485 1140 C 1491 1136 1499 1137 1499 1127 C 1503 1126 1504 1126 1508 1126 C 1508 1126 1512 1126 1512 1126 C 1514 1126 1518 1126 1519 1124 C 1521 1122 1520 1115 1516 1113 C 1509 1111 1504 1119 1499 1120 C 1490 1122 1478 1109 1471 1104 C 1469 1103 1461 1101 1461 1098 C 1460 1095 1464 1094 1466 1093 C 1474 1090 1479 1088 1486 1083 C 1486 1083 1512 1061 1512 1061 C 1512 1061 1549 1043 1549 1043 C 1549 1043 1564 1033 1564 1033 C 1566 1032 1569 1030 1571 1031 C 1575 1033 1571 1039 1569 1041 C 1569 1041 1543 1061 1543 1061 C 1545 1064 1546 1066 1549 1067 C 1552 1067 1556 1066 1557 1070 C 1558 1073 1557 1077 1556 1080 C 1556 1080 1549 1097 1549 1097 C 1548 1102 1549 1109 1544 1112 C 1540 1115 1536 1112 1530 1111 C 1531 1119 1535 1119 1542 1122 C 1542 1122 1554 1126 1554 1126 C 1554 1126 1571 1130 1571 1130 C 1584 1134 1594 1138 1605 1145 C 1613 1150 1620 1157 1628 1160 C 1640 1166 1653 1164 1664 1171 C 1672 1176 1682 1188 1681 1198 C 1680 1204 1676 1209 1677 1212 C 1678 1216 1690 1226 1693 1228 C 1699 1234 1710 1242 1718 1245 C 1724 1246 1727 1244 1734 1244 C 1734 1244 1749 1246 1749 1246 C 1756 1246 1759 1245 1765 1245 C 1782 1244 1789 1254 1804 1255 C 1820 1256 1831 1242 1840 1230 C 1844 1224 1849 1215 1857 1214 C 1866 1213 1872 1224 1873 1231 C 1874 1233 1874 1237 1875 1239 C 1878 1244 1887 1246 1892 1245 C 1890 1239 1886 1229 1886 1223 C 1886 1218 1889 1218 1889 1212 C 1889 1203 1883 1184 1891 1178 C 1895 1175 1899 1174 1903 1173 C 1900 1167 1894 1170 1888 1169 C 1883 1168 1880 1166 1876 1163 C 1876 1163 1855 1144 1855 1144 C 1847 1139 1843 1139 1839 1136 C 1839 1136 1831 1128 1831 1128 C 1824 1122 1818 1118 1811 1110 C 1799 1097 1803 1092 1796 1084 C 1793 1079 1788 1075 1782 1073 C 1778 1071 1774 1071 1772 1068 C 1769 1064 1768 1055 1768 1051 C 1768 1051 1769 1035 1769 1035 C 1769 1027 1769 1028 1770 1020 C 1765 1019 1762 1014 1759 1011 C 1759 1011 1738 992 1738 992 C 1724 982 1708 978 1692 971 C 1685 968 1674 964 1668 958 C 1660 948 1662 936 1658 930 C 1655 926 1651 926 1646 924 C 1642 922 1639 920 1636 917 C 1628 909 1623 901 1617 892 C 1614 886 1609 877 1603 874 C 1599 871 1587 869 1582 870 C 1578 871 1576 873 1569 874 C 1559 875 1550 873 1541 868 C 1535 866 1522 857 1516 860 C 1513 861 1512 865 1510 868 C 1507 875 1506 882 1500 888 C 1488 897 1465 889 1451 888 C 1446 888 1433 891 1431 896 C 1430 898 1431 902 1432 904 C 1432 908 1434 917 1434 921 C 1434 921 1429 938 1429 938 C 1426 958 1424 989 1411 1006 C 1404 1015 1391 1024 1381 1029 C 1381 1029 1367 1035 1367 1035 C 1363 1038 1362 1041 1359 1045 C 1359 1045 1350 1058 1350 1058 C 1348 1063 1348 1065 1344 1070 C 1340 1074 1335 1079 1330 1083 C 1328 1085 1323 1088 1322 1090 C 1321 1092 1324 1103 1325 1107 C 1325 1115 1320 1130 1329 1140 C 1334 1144 1339 1142 1344 1141 C 1347 1140 1356 1139 1357 1137 C 1359 1136 1360 1132 1361 1130 C 1362 1126 1368 1116 1369 1113 C 1369 1108 1365 1107 1364 1100 z"/> + <path id="stp" d="M 1586 175 C 1590 180 1597 184 1598 190 C 1599 194 1597 196 1597 201 C 1598 204 1601 211 1595 211 C 1590 211 1588 204 1581 201 C 1578 207 1576 215 1572 220 C 1566 226 1562 224 1556 230 C 1551 236 1552 242 1550 245 C 1549 248 1547 248 1546 251 C 1544 254 1543 262 1543 266 C 1541 271 1539 277 1539 282 C 1540 291 1546 299 1547 308 C 1542 306 1541 303 1538 299 C 1530 288 1528 274 1530 261 C 1532 255 1537 239 1528 236 C 1525 235 1522 237 1522 240 C 1520 245 1523 248 1521 258 C 1520 257 1518 255 1516 255 C 1513 255 1511 258 1510 260 C 1506 265 1503 270 1500 276 C 1495 286 1493 293 1491 304 C 1490 311 1493 316 1489 322 C 1483 334 1471 334 1460 329 C 1456 328 1452 326 1449 323 C 1441 312 1463 307 1455 295 C 1450 288 1442 289 1434 288 C 1429 287 1424 286 1419 289 C 1428 296 1432 302 1434 313 C 1435 318 1436 327 1438 331 C 1440 335 1443 334 1446 337 C 1449 339 1451 342 1452 346 C 1453 351 1454 365 1449 368 C 1447 369 1445 369 1443 369 C 1438 369 1431 368 1426 372 C 1423 374 1418 386 1413 392 C 1410 396 1402 404 1403 409 C 1404 413 1409 416 1412 419 C 1416 422 1418 424 1421 428 C 1431 438 1435 433 1443 436 C 1445 437 1450 439 1448 442 C 1446 446 1437 443 1434 442 C 1426 441 1420 441 1412 441 C 1403 441 1398 443 1388 440 C 1381 438 1374 430 1369 433 C 1366 435 1366 440 1367 443 C 1368 448 1372 454 1377 455 C 1382 457 1385 454 1391 460 C 1396 466 1398 468 1392 474 C 1391 475 1390 477 1388 477 C 1384 479 1372 473 1368 471 C 1361 468 1352 467 1346 461 C 1343 457 1341 452 1338 448 C 1336 444 1331 439 1330 434 C 1328 428 1331 419 1326 414 C 1324 411 1319 411 1314 408 C 1314 408 1303 400 1303 400 C 1297 395 1296 396 1289 389 C 1288 387 1283 382 1286 380 C 1289 376 1295 385 1302 386 C 1302 386 1313 386 1313 386 C 1313 386 1322 390 1322 390 C 1329 391 1351 394 1358 394 C 1365 394 1378 395 1384 392 C 1395 387 1409 365 1406 352 C 1405 349 1394 336 1392 334 C 1383 327 1369 324 1358 321 C 1358 321 1313 304 1313 304 C 1303 303 1291 306 1284 306 C 1281 306 1272 304 1272 300 C 1272 297 1275 298 1276 291 C 1272 290 1267 289 1263 292 C 1257 296 1255 305 1252 312 C 1250 316 1246 322 1243 326 C 1241 329 1238 333 1237 336 C 1236 339 1237 344 1237 347 C 1240 361 1249 363 1251 372 C 1251 372 1253 396 1253 396 C 1254 406 1256 416 1258 426 C 1262 439 1275 471 1280 483 C 1287 498 1297 513 1297 530 C 1297 540 1292 539 1292 553 C 1292 553 1293 561 1293 561 C 1293 574 1286 587 1279 598 C 1276 603 1269 613 1272 619 C 1276 624 1284 623 1289 625 C 1293 626 1297 629 1300 631 C 1292 639 1293 635 1285 636 C 1285 636 1273 641 1273 641 C 1268 643 1266 651 1262 655 C 1258 658 1254 658 1249 658 C 1249 658 1226 656 1226 656 C 1214 656 1205 659 1194 665 C 1200 677 1215 678 1227 678 C 1232 678 1239 677 1243 678 C 1255 680 1256 690 1262 698 C 1267 705 1272 707 1276 712 C 1282 718 1284 729 1284 737 C 1301 737 1320 739 1334 726 C 1343 717 1349 705 1356 695 C 1360 690 1364 685 1371 686 C 1377 687 1383 692 1390 694 C 1393 695 1396 695 1398 693 C 1401 691 1401 683 1401 680 C 1401 673 1402 667 1404 661 C 1414 632 1438 623 1466 623 C 1484 623 1504 613 1519 604 C 1531 597 1535 592 1550 588 C 1550 588 1590 581 1590 581 C 1590 581 1643 560 1643 560 C 1643 560 1693 548 1693 548 C 1702 544 1709 539 1717 534 C 1717 534 1736 524 1736 524 C 1736 524 1795 495 1795 495 C 1825 477 1854 457 1882 436 C 1909 416 1934 395 1959 373 C 1959 373 1978 356 1978 356 C 1978 356 2008 328 2008 328 C 2012 324 2017 317 2023 316 C 2023 316 2023 175 2023 175 C 2023 175 1586 175 1586 175 z"/> + <path id="stp-sc" d="M 1279 598 C 1276 603 1269 613 1272 619 C 1276 624 1284 623 1289 625 C 1293 626 1297 629 1300 631 C 1292 639 1293 635 1285 636 C 1285 636 1273 641 1273 641 C 1268 643 1266 651 1262 655 C 1258 658 1254 658 1249 658 C 1249 658 1226 656 1226 656 C 1214 656 1205 659 1194 665 C 1200 677 1215 678 1227 678 C 1232 678 1239 677 1243 678 C 1255 680 1256 690 1262 698 C 1267 705 1272 707 1276 712 C 1282 718 1284 729 1284 737 C 1301 737 1320 739 1334 726 C 1343 717 1349 705 1356 695 C 1360 690 1364 685 1371 686 C 1377 687 1383 692 1390 694 C 1393 695 1396 695 1398 693 C 1401 691 1401 683 1401 680 C 1401 673 1402 667 1404 661 C 1414 632 1340.57 568.188 1340.57 568.188 C 1340.57 568.188 1286 587 1279 598 z"/> + <path id="stp-nc" d="M 1768.68 297.625 C 1760.48 338.063 1829.46 251.188 1842.41 241.75 C 1842.41 241.75 2023 175 2023 175 C 2023 175 1586 175 1586 175 C 1590 180 1597 184 1598 190 C 1599 194 1597 196 1597 201 C 1598 204 1601 211 1595 211 C 1590 211 1588 204 1581 201 C 1578 207 1576 215 1572 220 C 1566 226 1562 224 1556 230 C 1551 236 1552 242 1550 245 C 1549 248 1547 248 1546 251 C 1544 254 1543 262 1543 266 C 1541 271 1539 277 1539 282 C 1540 291 1546 299 1547 308 C 1542 306 1541 303 1538 299 C 1530 288 1528 274 1530 261 C 1532 255 1537 239 1528 236 C 1525 235 1522 237 1522 240 C 1520 245 1523 248 1521 258 C 1520 257 1518 255 1516 255 C 1513 255 1511 258 1510 260 C 1506 265 1503 270 1500 276 C 1495 286 1493 293 1491 304 C 1490 311 1493 316 1489 322 C 1483 334 1471 334 1460 329 C 1456 328 1452 326 1449 323 C 1441 312 1463 307 1455 295 C 1450 288 1442 289 1434 288 C 1429 287 1424 286 1419 289 C 1428 296 1432 302 1434 313 C 1435 318 1436 327 1438 331 C 1440 335 1443 334 1446 337 C 1449 339 1451 342 1452 346 C 1453 351 1454 365 1449 368 C 1447 369 1445 369 1443 369 C 1438 369 1431 368 1426 372 C 1423 374 1418 386 1413 392 C 1410 396 1402 404 1403 409 C 1404 413 1409 416 1412 419 C 1416 422 1418 424 1421 428 C 1431 438 1435 433 1443 436 C 1445 437 1450 439 1448 442 C 1446 446 1437 443 1434 442 C 1426 441 1420 441 1412 441 C 1403 441 1398 443 1388 440 C 1381 438 1374 430 1369 433 C 1366 435 1366 440 1367 443 C 1368 448 1372 454 1377 455 C 1382 457 1385 454 1391 460 C 1396 466 1398 468 1392 474 C 1391 475 1390 477 1388 477 C 1384 479 1372 473 1368 471 C 1361 468 1352 467 1346 461 C 1343 457 1341 452 1338 448 C 1336 444 1331 439 1330 434 C 1328 428 1331 419 1326 414 C 1324 411 1319 411 1314 408 C 1314 408 1303 400 1303 400 C 1297 395 1296 396 1289 389 C 1288 387 1283 382 1286 380 C 1289 376 1295 385 1302 386 C 1302 386 1313 386 1313 386 C 1313 386 1322 390 1322 390 C 1329 391 1351 394 1358 394 C 1365 394 1378 395 1384 392 C 1395 387 1409 365 1406 352 C 1405 349 1394 336 1392 334 C 1383 327 1369 324 1358 321 C 1358 321 1313 304 1313 304 C 1303 303 1291 306 1284 306 C 1281 306 1272 304 1272 300 C 1272 297 1275 298 1276 291 C 1272 290 1267 289 1263 292 C 1257 296 1255 305 1252 312 C 1250 316 1246 322 1243 326 C 1241 329 1238 333 1237 336 C 1236 339 1237 344 1237 347 C 1240 361 1249 363 1251 372 C 1251 372 1253 396 1253 396 C 1254 406 1256 416 1258 426 C 1262 439 1275 471 1280 483 C 1287 498 1297 513 1297 530 C 1297 540 1462.17 478.25 1462.17 492.25 C 1467.38 490.563 1602.09 407.438 1595.15 407.438 C 1593.41 422.125 1728.8 338.938 1721.8 349.938"/> + <path id="syr" d="M 2022 1247 C 2020 1256 2012 1263 2005 1268 C 1988 1280 1980 1283 1959 1280 C 1958 1285 1953 1287 1950 1290 C 1950 1290 1934 1305 1934 1305 C 1918 1320 1892 1344 1871 1350 C 1862 1352 1859 1351 1850 1352 C 1850 1352 1842 1353 1842 1353 C 1842 1353 1831 1352 1831 1352 C 1798 1352 1766 1345 1733 1342 C 1709 1340 1683 1341 1659 1341 C 1645 1341 1636 1348 1624 1353 C 1614 1358 1609 1356 1600 1365 C 1591 1374 1592 1381 1588 1392 C 1582 1405 1577 1410 1576 1412 C 1576 1412 1574 1420 1574 1420 C 1574 1420 1568 1433 1568 1433 C 1568 1433 1572 1437 1572 1437 C 1572 1437 1573 1459 1573 1459 C 1580 1460 1579 1463 1579 1468 C 1579 1468 1581 1475 1581 1475 C 1583 1484 1582 1484 1582 1492 C 1582 1492 1584 1509 1584 1509 C 1584 1509 1584 1527 1584 1527 C 1584 1527 2023 1527 2023 1527 C 2023 1527 2023 1330 2023 1330 C 2023 1330 2023 1273 2023 1273 C 2023 1273 2022 1247 2022 1247 z"/> + <path id="swi" d="M 778 1102 C 786 1104 791 1096 798 1099 C 809 1103 801 1114 814 1113 C 822 1111 825 1108 834 1112 C 842 1114 846 1121 851 1120 C 856 1120 857 1113 865 1112 C 870 1111 883 1114 886 1111 C 890 1109 890 1101 888 1098 C 887 1097 885 1094 884 1093 C 880 1089 877 1086 874 1081 C 868 1070 869 1061 853 1061 C 853 1061 827 1063 827 1063 C 823 1063 818 1063 814 1065 C 811 1066 797 1081 793 1085 C 793 1085 786 1092 786 1092 C 782 1096 780 1095 778 1102 z"/> + <path id="ukr" d="M 1275 1047 C 1293 1047 1303 1065 1311 1078 C 1311 1078 1318 1088 1318 1088 C 1324 1084 1337 1074 1341 1068 C 1344 1064 1344 1062 1347 1058 C 1347 1058 1362 1035 1362 1035 C 1367 1030 1371 1029 1377 1027 C 1383 1024 1390 1020 1395 1017 C 1415 1003 1417 987 1421 965 C 1421 965 1426 935 1426 935 C 1428 924 1431 925 1429 912 C 1429 909 1427 899 1425 897 C 1424 895 1419 895 1417 894 C 1409 893 1402 893 1394 893 C 1383 893 1365 898 1354 901 C 1354 901 1275 921 1275 921 C 1275 921 1257 926 1257 926 C 1249 928 1241 929 1237 938 C 1234 947 1237 953 1233 965 C 1232 968 1229 973 1230 976 C 1233 983 1249 989 1256 995 C 1266 1005 1275 1025 1275 1039 C 1275 1039 1275 1047 1275 1047 z"/> + <path id="lvn" d="M 1190 668 C 1187 675 1186 680 1189 688 C 1192 699 1199 697 1203 702 C 1206 705 1206 708 1206 711 C 1207 719 1207 731 1204 738 C 1202 742 1197 749 1192 747 C 1185 745 1175 723 1164 725 C 1159 726 1157 731 1155 735 C 1151 744 1145 758 1145 767 C 1145 770 1145 777 1146 780 C 1148 782 1152 782 1157 789 C 1159 794 1160 800 1165 803 C 1171 808 1178 805 1185 815 C 1191 823 1191 835 1188 844 C 1187 848 1184 853 1185 857 C 1188 866 1202 873 1209 879 C 1212 881 1214 883 1217 886 C 1225 881 1236 871 1243 864 C 1243 864 1251 855 1251 855 C 1257 849 1263 850 1268 848 C 1275 843 1274 833 1274 826 C 1274 817 1271 810 1272 801 C 1273 796 1276 793 1276 786 C 1276 780 1273 778 1274 771 C 1274 771 1278 757 1278 757 C 1280 747 1281 732 1278 722 C 1274 710 1266 710 1260 701 C 1255 695 1253 686 1246 682 C 1240 679 1231 682 1224 682 C 1218 682 1207 680 1202 677 C 1198 675 1194 671 1190 668 z"/> + <path id="war" d="M 1180 860 C 1162 885 1123 876 1100 892 C 1073 911 1079 940 1096 963 C 1098 966 1107 976 1109 978 C 1113 981 1125 983 1130 983 C 1138 983 1146 978 1152 973 C 1155 970 1157 966 1160 965 C 1167 962 1176 971 1186 973 C 1200 977 1205 969 1213 967 C 1218 967 1222 970 1226 972 C 1230 962 1231 958 1232 947 C 1232 942 1228 923 1226 918 C 1226 918 1221 908 1221 908 C 1219 901 1219 894 1214 888 C 1209 881 1201 878 1194 873 C 1189 869 1186 863 1180 860 z"/> + <path id="pru" d="M 1146 788 C 1147 796 1148 812 1141 817 C 1137 821 1131 817 1127 821 C 1125 824 1125 827 1123 830 C 1122 833 1119 836 1116 838 C 1109 843 1100 847 1093 840 C 1090 837 1090 835 1090 831 C 1087 831 1083 830 1080 830 C 1069 831 1058 838 1048 842 C 1031 848 1028 850 1010 853 C 1010 853 1014 883 1014 883 C 1016 891 1018 893 1019 898 C 1021 904 1019 909 1022 912 C 1025 915 1030 916 1033 916 C 1033 916 1061 918 1061 918 C 1064 918 1072 919 1074 917 C 1074 917 1081 904 1081 904 C 1085 896 1092 890 1099 885 C 1111 878 1130 875 1144 872 C 1154 870 1164 868 1172 861 C 1176 857 1179 851 1181 846 C 1182 841 1183 839 1183 834 C 1183 834 1183 830 1183 830 C 1183 824 1182 818 1177 815 C 1172 812 1168 814 1161 808 C 1151 799 1155 792 1146 788 z"/> + <path id="sil" d="M 960 935 C 960 935 970 964 970 964 C 970 964 997 957 997 957 C 1004 956 1012 954 1019 956 C 1029 960 1036 969 1044 975 C 1053 982 1055 981 1065 984 C 1080 989 1090 995 1104 983 C 1104 983 1088 962 1088 962 C 1079 949 1075 937 1075 921 C 1075 921 1039 921 1039 921 C 1039 921 1027 919 1027 919 C 1027 919 1019 916 1019 916 C 1019 916 1012 921 1012 921 C 1012 921 1002 926 1002 926 C 1002 926 960 935 960 935 z"/> + <path id="ber" d="M 939 938 C 939 938 969 929 969 929 C 969 929 1001 923 1001 923 C 1001 923 1017 913 1017 913 C 1017 913 1016 899 1016 899 C 1016 899 1010 884 1010 884 C 1008 875 1007 862 1007 853 C 1001 852 995 851 990 847 C 988 846 985 843 986 840 C 987 838 988 837 989 836 C 988 835 987 834 985 833 C 980 832 966 839 959 841 C 955 842 949 842 947 845 C 947 845 943 868 943 868 C 941 880 936 900 936 912 C 936 920 936 931 939 938 z"/> + <path id="kie" d="M 823 916 C 842 925 854 923 872 938 C 878 944 883 947 888 954 C 890 956 892 960 895 960 C 899 961 903 956 906 954 C 913 949 918 950 927 945 C 929 943 933 941 933 939 C 935 937 934 934 933 932 C 933 932 932 918 932 918 C 932 913 932 912 933 908 C 933 908 936 886 936 886 C 938 874 942 860 942 848 C 942 848 926 848 926 848 C 928 845 933 839 933 836 C 932 832 926 829 923 825 C 918 818 915 812 915 803 C 915 803 900 803 900 803 C 898 803 894 803 893 804 C 889 807 893 819 893 823 C 893 826 891 839 890 842 C 889 844 888 846 886 848 C 876 857 871 837 859 845 C 855 848 856 853 856 858 C 856 869 852 871 849 880 C 848 884 848 889 845 893 C 838 904 826 902 823 916 z"/> + <path id="ruh" d="M 822 920 C 820 928 819 933 815 940 C 813 943 810 948 810 951 C 809 957 817 969 815 978 C 813 984 810 984 808 988 C 805 998 813 1013 817 1015 C 821 1017 831 1013 834 1010 C 838 1007 841 1000 844 996 C 849 989 856 980 864 976 C 873 971 877 974 884 971 C 886 970 890 968 891 966 C 892 962 882 952 879 949 C 863 933 857 932 837 925 C 837 925 822 920 822 920 z"/> + <path id="mun" d="M 820 1020 C 820 1020 824 1031 824 1031 C 824 1031 821 1045 821 1045 C 821 1045 819 1058 819 1058 C 829 1058 833 1058 843 1057 C 847 1056 855 1054 859 1054 C 865 1055 868 1059 872 1060 C 875 1062 879 1060 883 1062 C 887 1064 889 1067 894 1068 C 894 1068 919 1066 919 1066 C 932 1066 937 1066 950 1069 C 950 1065 948 1059 950 1055 C 952 1049 956 1049 960 1047 C 967 1043 966 1040 975 1039 C 974 1033 971 1029 967 1024 C 967 1024 948 1001 948 1001 C 942 993 938 985 947 976 C 949 975 952 973 954 971 C 958 969 962 968 966 966 C 966 966 957 936 957 936 C 953 937 939 942 935 944 C 935 944 924 950 924 950 C 919 953 913 953 908 957 C 900 962 895 969 887 973 C 876 979 871 974 859 984 C 848 992 845 1004 837 1012 C 832 1017 826 1017 820 1020 z"/> + <path id="rum" d="M 1277 1053 C 1276 1059 1276 1068 1272 1073 C 1268 1079 1259 1080 1258 1086 C 1257 1092 1263 1097 1266 1101 C 1276 1111 1287 1119 1280 1134 C 1275 1143 1265 1147 1256 1150 C 1256 1150 1203 1163 1203 1163 C 1193 1167 1180 1178 1187 1190 C 1189 1192 1192 1193 1194 1195 C 1198 1197 1198 1200 1201 1201 C 1205 1203 1226 1205 1231 1205 C 1242 1205 1255 1206 1266 1204 C 1266 1204 1287 1197 1287 1197 C 1303 1194 1323 1194 1339 1199 C 1339 1193 1341 1177 1343 1172 C 1346 1167 1352 1163 1355 1158 C 1357 1154 1357 1150 1357 1145 C 1349 1146 1336 1151 1329 1148 C 1321 1143 1317 1130 1317 1122 C 1317 1122 1318 1110 1318 1110 C 1318 1096 1314 1093 1307 1082 C 1299 1069 1293 1057 1277 1053 z"/> + <path id="bul" d="M 1188 1199 C 1187 1205 1185 1210 1187 1216 C 1189 1223 1196 1231 1196 1237 C 1197 1245 1189 1244 1190 1255 C 1193 1268 1204 1282 1189 1291 C 1195 1299 1199 1297 1208 1294 C 1208 1294 1224 1288 1224 1288 C 1236 1284 1244 1282 1249 1297 C 1250 1301 1250 1303 1250 1307 C 1250 1307 1250 1311 1250 1311 C 1250 1311 1269 1311 1269 1311 C 1272 1311 1276 1313 1279 1311 C 1281 1310 1282 1308 1283 1306 C 1292 1293 1281 1282 1293 1270 C 1306 1258 1316 1264 1328 1262 C 1328 1262 1320 1248 1320 1248 C 1319 1245 1321 1241 1322 1238 C 1325 1229 1323 1223 1326 1218 C 1331 1210 1335 1216 1338 1206 C 1324 1199 1299 1201 1284 1204 C 1276 1206 1273 1209 1263 1211 C 1263 1211 1242 1212 1242 1212 C 1242 1212 1229 1211 1229 1211 C 1222 1211 1207 1210 1201 1208 C 1193 1206 1195 1202 1188 1199 z"/> + <path id="bul-sc" d="M 1244.54 1260.06 C 1215.87 1268 1224 1288 1224 1288 C 1236 1284 1244 1282 1249 1297 C 1250 1301 1250 1303 1250 1307 C 1250 1307 1250 1311 1250 1311 C 1250 1311 1269 1311 1269 1311 C 1272 1311 1276 1313 1279 1311 C 1281 1310 1282 1308 1283 1306 C 1292 1293 1281 1282 1293 1270 C 1306 1258 1264.65 1252.44 1244.54 1260.06 z"/> + <path id="bul-ec" d="M 1293 1270 C 1306 1258 1316 1264 1328 1262 C 1328 1262 1320 1248 1320 1248 C 1319 1245 1321 1241 1322 1238 C 1325 1229 1323 1223 1326 1218 C 1331 1210 1335 1216 1338 1206 C 1324 1199 1299 1201 1284 1204 C 1276 1206 1273 1209 1263 1211 C 1263 1211 1281 1282 1293 1270 z"/> + <path id="gre" d="M 1155 1385 C 1155 1385 1155 1387 1155 1387 C 1153 1388 1149 1389 1148 1391 C 1146 1395 1152 1403 1155 1404 C 1162 1406 1184 1400 1188 1400 C 1191 1401 1193 1402 1196 1403 C 1200 1405 1207 1407 1206 1413 C 1205 1416 1203 1418 1200 1418 C 1200 1418 1191 1413 1191 1413 C 1186 1411 1178 1409 1173 1411 C 1173 1411 1166 1413 1166 1413 C 1162 1414 1160 1414 1158 1416 C 1155 1418 1156 1422 1158 1425 C 1161 1430 1165 1433 1167 1439 C 1167 1439 1172 1459 1172 1459 C 1173 1458 1175 1455 1176 1454 C 1185 1448 1189 1467 1191 1471 C 1193 1468 1195 1461 1199 1461 C 1202 1461 1203 1464 1209 1465 C 1208 1457 1204 1448 1201 1441 C 1199 1438 1197 1435 1201 1432 C 1206 1428 1214 1437 1217 1432 C 1220 1429 1212 1426 1211 1422 C 1210 1418 1213 1415 1217 1415 C 1223 1415 1227 1419 1232 1421 C 1232 1421 1231 1414 1231 1414 C 1230 1409 1232 1407 1228 1403 C 1223 1398 1203 1395 1199 1388 C 1196 1384 1199 1382 1199 1379 C 1200 1376 1198 1374 1198 1372 C 1199 1365 1208 1368 1212 1369 C 1208 1358 1195 1350 1192 1338 C 1190 1329 1198 1325 1201 1326 C 1204 1328 1202 1331 1205 1335 C 1207 1339 1212 1341 1216 1343 C 1216 1343 1214 1336 1214 1336 C 1214 1336 1230 1344 1230 1344 C 1230 1344 1222 1331 1222 1331 C 1222 1331 1233 1332 1233 1332 C 1231 1330 1226 1324 1226 1322 C 1225 1319 1229 1318 1231 1317 C 1237 1315 1235 1313 1243 1313 C 1243 1307 1245 1296 1238 1292 C 1234 1291 1222 1296 1218 1297 C 1218 1297 1187 1308 1187 1308 C 1187 1308 1166 1313 1166 1313 C 1164 1314 1159 1315 1158 1316 C 1155 1318 1156 1323 1156 1326 C 1156 1331 1154 1335 1151 1339 C 1146 1344 1141 1345 1138 1349 C 1136 1351 1135 1353 1134 1355 C 1132 1358 1130 1360 1129 1363 C 1127 1371 1134 1379 1140 1382 C 1145 1385 1145 1381 1155 1385 z M 1208 1382 C 1211 1386 1222 1396 1226 1399 C 1229 1400 1232 1401 1235 1401 C 1235 1390 1229 1393 1222 1389 C 1216 1385 1216 1382 1208 1382 z"/> + <path id="smy" d="M 1636 1288 C 1625 1299 1615 1296 1600 1296 C 1584 1296 1573 1300 1561 1311 C 1546 1324 1537 1340 1516 1344 C 1513 1344 1510 1345 1507 1345 C 1498 1344 1483 1338 1475 1340 C 1470 1341 1465 1346 1461 1348 C 1451 1355 1437 1365 1425 1367 C 1418 1369 1409 1364 1401 1365 C 1395 1365 1391 1368 1386 1368 C 1379 1368 1376 1365 1371 1365 C 1366 1364 1362 1367 1358 1368 C 1358 1368 1349 1368 1349 1368 C 1342 1369 1337 1368 1331 1364 C 1328 1362 1323 1358 1319 1358 C 1316 1359 1311 1365 1311 1368 C 1311 1371 1313 1373 1313 1376 C 1314 1379 1312 1381 1311 1384 C 1311 1388 1312 1390 1311 1393 C 1311 1396 1307 1399 1305 1401 C 1312 1403 1321 1406 1322 1414 C 1322 1419 1320 1417 1323 1428 C 1323 1428 1331 1427 1331 1427 C 1333 1434 1333 1433 1330 1440 C 1330 1440 1349 1436 1349 1436 C 1351 1436 1354 1437 1355 1439 C 1355 1442 1350 1448 1349 1451 C 1355 1449 1355 1447 1359 1446 C 1363 1445 1366 1447 1370 1447 C 1372 1448 1375 1447 1377 1448 C 1381 1450 1383 1454 1385 1457 C 1389 1460 1395 1462 1400 1462 C 1404 1462 1416 1458 1418 1455 C 1421 1450 1418 1435 1432 1432 C 1443 1431 1460 1441 1469 1446 C 1474 1449 1478 1452 1484 1452 C 1493 1452 1512 1447 1518 1441 C 1524 1435 1528 1424 1535 1420 C 1544 1415 1552 1425 1558 1422 C 1565 1418 1564 1405 1575 1408 C 1579 1402 1583 1394 1586 1388 C 1590 1375 1588 1369 1603 1358 C 1611 1353 1614 1353 1622 1350 C 1622 1350 1640 1342 1640 1342 C 1644 1340 1652 1339 1654 1335 C 1657 1322 1641 1315 1637 1308 C 1634 1303 1637 1293 1636 1288 z"/> + <path id="alb" d="M 1149 1316 C 1136 1313 1137 1308 1136 1297 C 1134 1284 1137 1271 1125 1262 C 1123 1261 1120 1259 1118 1258 C 1112 1257 1101 1264 1101 1273 C 1100 1280 1110 1281 1112 1290 C 1113 1298 1107 1314 1107 1326 C 1107 1329 1107 1334 1108 1337 C 1109 1341 1121 1352 1125 1353 C 1130 1354 1128 1349 1133 1344 C 1137 1340 1142 1339 1145 1336 C 1151 1331 1150 1323 1149 1316 z"/> + <path id="ser" d="M 1189 1301 C 1187 1298 1181 1293 1183 1288 C 1185 1285 1189 1285 1190 1280 C 1191 1274 1184 1263 1184 1254 C 1184 1251 1184 1248 1185 1245 C 1187 1242 1190 1240 1190 1236 C 1189 1231 1184 1226 1181 1218 C 1178 1209 1181 1208 1181 1200 C 1181 1196 1181 1185 1177 1183 C 1176 1182 1173 1182 1171 1182 C 1171 1182 1144 1180 1144 1180 C 1137 1179 1135 1175 1131 1176 C 1128 1176 1125 1179 1121 1181 C 1115 1183 1111 1176 1107 1181 C 1106 1181 1104 1184 1104 1185 C 1100 1194 1107 1196 1107 1204 C 1106 1208 1104 1212 1103 1216 C 1101 1222 1101 1232 1104 1238 C 1110 1249 1126 1253 1134 1262 C 1140 1269 1140 1274 1141 1283 C 1141 1283 1142 1292 1142 1292 C 1142 1296 1141 1303 1144 1307 C 1146 1309 1151 1309 1154 1309 C 1165 1309 1174 1302 1189 1301 z"/> + <path id="bud" d="M 1121 1029 C 1121 1031 1121 1033 1120 1035 C 1118 1041 1113 1038 1109 1039 C 1106 1040 1104 1041 1102 1043 C 1092 1052 1092 1061 1085 1069 C 1079 1075 1074 1076 1069 1080 C 1065 1083 1060 1089 1056 1093 C 1052 1097 1047 1100 1047 1106 C 1047 1106 1048 1113 1048 1113 C 1050 1124 1054 1132 1063 1139 C 1069 1143 1075 1144 1079 1148 C 1088 1155 1088 1174 1103 1173 C 1103 1173 1111 1171 1111 1171 C 1113 1172 1115 1174 1117 1174 C 1123 1176 1125 1169 1132 1169 C 1136 1170 1139 1173 1144 1174 C 1151 1175 1155 1175 1162 1175 C 1162 1175 1169 1176 1169 1176 C 1181 1177 1183 1170 1191 1163 C 1196 1160 1200 1157 1206 1156 C 1206 1156 1258 1143 1258 1143 C 1268 1139 1276 1135 1275 1123 C 1274 1113 1259 1104 1253 1095 C 1253 1095 1249 1087 1249 1087 C 1249 1087 1234 1065 1234 1065 C 1230 1061 1227 1062 1221 1056 C 1213 1047 1208 1039 1197 1033 C 1187 1028 1182 1031 1173 1028 C 1166 1026 1162 1021 1154 1021 C 1144 1021 1131 1027 1121 1029 z"/> + <path id="gal" d="M 1083 997 C 1083 997 1085 1015 1085 1015 C 1094 1015 1098 1015 1106 1018 C 1112 1021 1115 1025 1120 1025 C 1123 1025 1128 1023 1131 1022 C 1139 1020 1146 1018 1155 1018 C 1164 1018 1166 1023 1174 1025 C 1182 1027 1189 1024 1201 1031 C 1212 1038 1216 1046 1224 1054 C 1229 1059 1232 1058 1238 1065 C 1243 1071 1246 1078 1252 1083 C 1258 1073 1266 1074 1269 1066 C 1270 1062 1269 1047 1269 1042 C 1269 1030 1262 1010 1254 1001 C 1249 996 1240 993 1234 988 C 1229 985 1220 975 1216 974 C 1209 973 1205 980 1193 981 C 1186 981 1180 978 1174 975 C 1172 974 1166 971 1163 971 C 1160 971 1157 976 1155 978 C 1151 982 1145 985 1140 988 C 1134 990 1130 990 1124 989 C 1121 989 1113 987 1111 987 C 1108 988 1105 991 1100 994 C 1096 996 1088 997 1083 997 z"/> + <path id="vie" d="M 1117 1036 C 1117 1034 1118 1031 1117 1029 C 1116 1028 1113 1026 1111 1025 C 1104 1021 1099 1019 1091 1019 C 1091 1019 1074 1020 1074 1020 C 1067 1019 1069 1016 1057 1016 C 1057 1016 1045 1018 1045 1018 C 1040 1018 1033 1013 1025 1019 C 1020 1024 1016 1031 1012 1037 C 1010 1042 1005 1049 1003 1054 C 1001 1061 1002 1078 1005 1085 C 1006 1088 1007 1090 1009 1092 C 1012 1097 1017 1107 1023 1108 C 1027 1108 1031 1104 1035 1102 C 1035 1102 1044 1100 1044 1100 C 1052 1096 1059 1084 1065 1078 C 1072 1072 1078 1073 1084 1064 C 1087 1060 1088 1057 1090 1053 C 1093 1048 1101 1037 1107 1036 C 1110 1035 1114 1036 1117 1036 z"/> + <path id="boh" d="M 1081 1016 C 1081 1016 1079 997 1079 997 C 1079 997 1083 997 1083 997 C 1083 997 1075 995 1075 995 C 1075 995 1063 990 1063 990 C 1056 988 1053 988 1046 984 C 1034 977 1023 959 1008 961 C 999 963 971 970 963 974 C 959 976 952 979 950 983 C 947 988 950 993 953 997 C 953 997 977 1027 977 1027 C 979 1031 981 1039 986 1040 C 990 1041 995 1041 999 1041 C 1001 1041 1004 1042 1005 1041 C 1008 1040 1011 1033 1012 1031 C 1017 1023 1025 1011 1036 1012 C 1040 1013 1042 1014 1046 1014 C 1050 1014 1053 1012 1060 1012 C 1067 1012 1069 1015 1073 1016 C 1075 1017 1079 1016 1081 1016 z"/> + <path id="tyr" d="M 875 1067 C 878 1085 889 1086 894 1098 C 898 1108 892 1109 892 1122 C 892 1129 895 1137 903 1135 C 908 1135 915 1130 917 1125 C 918 1122 917 1119 920 1115 C 926 1109 936 1108 944 1108 C 947 1108 957 1108 959 1107 C 961 1106 962 1104 964 1102 C 967 1100 970 1099 973 1099 C 977 1098 981 1098 985 1099 C 988 1099 991 1100 994 1099 C 998 1097 1001 1088 1000 1084 C 999 1065 995 1064 1003 1045 C 994 1045 981 1042 973 1046 C 970 1047 967 1050 964 1052 C 962 1053 958 1054 957 1056 C 952 1060 962 1072 952 1075 C 950 1076 946 1075 944 1074 C 939 1073 936 1073 931 1073 C 931 1073 910 1073 910 1073 C 904 1073 897 1075 891 1074 C 884 1073 885 1068 875 1067 z"/> + <path id="tri" d="M 1003 1091 C 1001 1096 998 1102 993 1103 C 987 1104 968 1097 964 1109 C 966 1110 968 1112 970 1114 C 973 1120 969 1134 967 1140 C 966 1145 968 1144 965 1152 C 964 1157 962 1168 969 1169 C 972 1170 975 1165 976 1163 C 980 1156 980 1153 989 1154 C 989 1157 989 1159 988 1162 C 986 1168 982 1170 986 1181 C 990 1193 1002 1199 1011 1207 C 1016 1212 1018 1214 1024 1219 C 1024 1219 1047 1234 1047 1234 C 1047 1234 1064 1248 1064 1248 C 1064 1248 1081 1260 1081 1260 C 1086 1263 1090 1268 1095 1270 C 1097 1260 1102 1257 1110 1253 C 1101 1243 1094 1239 1095 1224 C 1095 1214 1099 1210 1100 1205 C 1100 1200 1097 1197 1096 1192 C 1096 1187 1099 1182 1100 1177 C 1095 1175 1091 1174 1088 1169 C 1084 1164 1083 1156 1078 1151 C 1073 1147 1068 1148 1059 1140 C 1047 1131 1046 1119 1043 1105 C 1039 1105 1037 1105 1034 1107 C 1031 1108 1027 1112 1023 1112 C 1014 1111 1010 1098 1003 1091 z"/> + <path id="fin" d="M 1181 440 C 1190 443 1204 455 1199 465 C 1199 465 1189 478 1189 478 C 1189 478 1175 500 1175 500 C 1175 500 1153 527 1153 527 C 1148 534 1139 544 1140 553 C 1140 553 1142 562 1142 562 C 1142 562 1143 572 1143 572 C 1143 572 1147 586 1147 586 C 1147 593 1142 600 1142 608 C 1142 612 1142 618 1146 622 C 1148 624 1151 624 1154 626 C 1154 626 1163 631 1163 631 C 1163 631 1170 633 1170 633 C 1175 635 1176 639 1181 640 C 1186 640 1200 636 1205 635 C 1205 635 1238 622 1238 622 C 1244 620 1258 617 1262 614 C 1268 611 1271 605 1275 599 C 1282 588 1290 572 1289 559 C 1289 555 1288 553 1288 549 C 1289 539 1294 537 1294 528 C 1292 512 1283 498 1276 483 C 1269 468 1260 443 1255 427 C 1252 417 1250 405 1249 394 C 1249 389 1248 376 1247 372 C 1246 367 1241 364 1237 357 C 1234 350 1232 341 1232 333 C 1228 336 1224 334 1224 329 C 1223 325 1227 318 1227 312 C 1226 301 1208 291 1199 299 C 1196 302 1196 308 1195 312 C 1193 323 1191 330 1179 332 C 1171 334 1157 331 1149 328 C 1149 328 1139 323 1139 323 C 1135 322 1132 323 1131 327 C 1130 332 1134 334 1137 337 C 1145 343 1153 350 1159 358 C 1171 374 1173 391 1177 410 C 1177 410 1181 440 1181 440 z"/> + <path id="swe" d="M 1128 338 C 1128 338 1126 348 1126 348 C 1120 349 1118 350 1112 351 C 1109 351 1104 351 1101 352 C 1098 355 1101 358 1098 362 C 1095 366 1091 362 1087 367 C 1082 373 1075 395 1071 403 C 1068 409 1064 416 1059 422 C 1055 428 1052 430 1048 437 C 1048 437 1043 456 1043 456 C 1043 456 1032 478 1032 478 C 1032 478 1028 494 1028 494 C 1023 504 1015 499 1008 505 C 1006 508 1004 513 1003 517 C 999 529 999 540 999 552 C 999 552 1000 567 1000 567 C 1000 567 1000 581 1000 581 C 1000 586 1000 589 998 593 C 994 601 991 599 991 610 C 991 623 987 636 984 649 C 981 657 978 676 970 679 C 967 680 964 679 961 679 C 961 689 964 693 966 703 C 966 703 973 731 973 731 C 975 736 981 751 982 755 C 982 760 978 769 979 774 C 979 774 982 786 982 786 C 984 794 980 803 993 801 C 1005 800 1003 794 1009 786 C 1014 780 1019 778 1026 778 C 1030 778 1037 779 1041 777 C 1045 774 1048 768 1049 764 C 1055 750 1054 741 1055 727 C 1055 727 1060 704 1060 704 C 1060 702 1060 699 1059 697 C 1058 696 1056 694 1057 692 C 1057 690 1060 689 1062 689 C 1067 688 1072 687 1077 684 C 1086 680 1090 674 1094 665 C 1097 659 1099 657 1099 650 C 1098 635 1083 627 1078 620 C 1075 616 1074 610 1074 605 C 1074 592 1078 571 1083 559 C 1086 553 1095 542 1099 537 C 1112 523 1128 517 1139 498 C 1144 489 1138 486 1139 477 C 1140 464 1149 444 1162 439 C 1166 438 1170 438 1174 438 C 1174 438 1171 417 1171 417 C 1169 402 1165 380 1158 367 C 1151 355 1139 345 1128 338 z"/> + <path id="nwy" d="M 1198 263 C 1194 266 1193 270 1192 274 C 1189 282 1188 287 1182 292 C 1180 283 1183 280 1183 272 C 1183 270 1183 267 1181 266 C 1178 265 1175 267 1173 268 C 1166 275 1167 275 1158 280 C 1158 280 1133 298 1133 298 C 1128 300 1123 302 1118 303 C 1115 304 1111 304 1108 305 C 1104 307 1104 312 1100 315 C 1097 317 1095 316 1093 317 C 1089 319 1087 324 1086 328 C 1088 330 1090 332 1089 335 C 1088 337 1083 342 1081 344 C 1081 344 1063 367 1063 367 C 1063 367 1045 394 1045 394 C 1045 394 1024 415 1024 415 C 1019 422 1015 431 1010 439 C 1005 447 995 464 992 472 C 992 472 991 483 991 483 C 983 480 980 483 974 488 C 971 491 967 496 965 499 C 964 502 964 506 961 508 C 958 510 952 509 947 512 C 941 515 937 521 933 522 C 929 523 928 521 925 521 C 921 520 918 522 916 526 C 916 526 920 528 920 528 C 915 534 912 532 905 535 C 906 537 908 541 907 542 C 905 545 900 541 894 542 C 888 544 882 551 879 556 C 871 567 869 577 867 590 C 866 593 865 601 865 604 C 867 609 872 610 874 614 C 876 619 871 620 867 621 C 867 621 871 624 871 624 C 879 633 863 634 861 637 C 860 638 860 642 860 644 C 864 643 878 639 879 646 C 879 650 874 653 872 655 C 865 660 859 665 864 674 C 866 677 869 681 872 683 C 874 685 877 686 880 688 C 899 697 913 679 929 672 C 934 669 939 668 944 664 C 946 663 950 658 953 658 C 957 658 960 668 961 672 C 964 672 968 674 970 671 C 972 668 974 659 975 656 C 978 646 984 626 984 616 C 984 612 984 606 985 602 C 987 597 991 595 993 589 C 994 584 994 578 994 573 C 994 573 992 546 992 546 C 992 536 995 516 1000 507 C 1002 502 1006 498 1011 496 C 1016 494 1020 495 1022 491 C 1022 491 1026 476 1026 476 C 1026 476 1037 453 1037 453 C 1037 453 1043 434 1043 434 C 1046 427 1053 421 1058 414 C 1067 400 1070 386 1077 372 C 1081 364 1084 358 1093 357 C 1092 343 1110 344 1120 344 C 1121 340 1122 336 1123 332 C 1124 326 1127 318 1134 316 C 1139 315 1144 319 1149 321 C 1156 324 1165 326 1172 326 C 1176 326 1181 326 1184 323 C 1191 317 1185 298 1199 291 C 1201 290 1203 290 1205 290 C 1217 289 1232 299 1234 312 C 1234 317 1231 324 1230 330 C 1241 325 1247 308 1251 297 C 1249 298 1243 300 1241 299 C 1237 299 1229 292 1227 288 C 1232 287 1244 285 1246 281 C 1251 273 1234 268 1229 268 C 1222 269 1223 277 1219 279 C 1216 280 1215 277 1214 275 C 1214 271 1218 257 1210 259 C 1203 260 1207 278 1201 280 C 1197 281 1197 275 1197 273 C 1197 273 1198 263 1198 263 z"/> + <path id="hol" d="M 768 906 C 778 917 791 914 799 922 C 804 928 800 932 804 943 C 804 943 806 943 806 943 C 809 937 813 931 815 924 C 816 918 817 911 820 906 C 824 900 834 897 839 892 C 841 889 841 883 843 879 C 847 866 850 867 850 850 C 850 850 823 849 823 849 C 815 850 801 861 796 866 C 786 876 790 890 780 900 C 776 903 772 904 768 906 z"/> + <path id="bel" d="M 715 931 C 715 931 747 950 747 950 C 760 958 761 961 777 969 C 782 972 798 980 804 980 C 809 979 808 972 807 968 C 805 954 798 945 796 940 C 794 934 796 929 793 926 C 791 923 785 922 782 921 C 782 921 775 918 775 918 C 769 915 766 914 761 908 C 754 910 745 912 738 913 C 733 914 726 914 722 917 C 718 921 717 926 715 931 z"/> + <path id="ven" d="M 957 1271 C 959 1266 960 1265 964 1260 C 966 1258 969 1255 969 1252 C 969 1250 965 1243 964 1240 C 959 1228 962 1222 951 1210 C 941 1199 927 1193 928 1176 C 928 1168 932 1168 933 1164 C 934 1160 928 1155 935 1150 C 938 1148 949 1143 952 1142 C 955 1141 959 1141 961 1139 C 962 1137 964 1125 965 1122 C 965 1120 965 1118 963 1117 C 961 1115 950 1114 947 1114 C 942 1114 929 1115 926 1119 C 924 1121 924 1123 923 1126 C 918 1134 910 1143 900 1142 C 894 1141 892 1136 888 1136 C 886 1136 883 1139 881 1141 C 877 1146 873 1153 872 1160 C 872 1167 879 1174 884 1178 C 891 1184 897 1186 903 1190 C 912 1196 920 1205 926 1213 C 934 1224 937 1231 942 1244 C 942 1244 945 1260 945 1260 C 946 1264 953 1269 957 1271 z"/> + <path id="pie" d="M 804 1114 C 803 1117 801 1120 802 1124 C 803 1128 807 1130 806 1135 C 805 1138 801 1142 801 1146 C 800 1151 804 1156 805 1161 C 806 1168 800 1176 806 1184 C 807 1186 809 1188 811 1189 C 815 1191 823 1187 826 1184 C 832 1179 839 1173 848 1174 C 856 1175 860 1182 869 1186 C 871 1183 875 1178 874 1175 C 874 1171 869 1169 869 1161 C 869 1149 878 1138 887 1131 C 887 1131 886 1119 886 1119 C 886 1119 866 1116 866 1116 C 859 1117 860 1129 851 1127 C 844 1126 838 1116 830 1115 C 821 1114 814 1125 804 1114 z"/> + <path id="tus" d="M 878 1178 C 877 1180 873 1185 873 1188 C 873 1191 875 1194 876 1201 C 877 1208 877 1215 878 1221 C 882 1232 891 1245 899 1253 C 899 1253 914 1239 914 1239 C 914 1239 932 1229 932 1229 C 927 1220 918 1207 911 1200 C 901 1191 899 1192 889 1186 C 889 1186 878 1178 878 1178 z"/> + <path id="rom" d="M 973 1291 C 973 1291 951 1271 951 1271 C 951 1271 942 1262 942 1262 C 942 1262 939 1249 939 1249 C 939 1249 934 1233 934 1233 C 925 1235 916 1241 909 1246 C 907 1249 903 1252 903 1256 C 903 1261 911 1268 914 1272 C 921 1280 926 1290 936 1295 C 941 1297 945 1296 950 1297 C 956 1298 958 1301 961 1300 C 961 1300 973 1291 973 1291 z"/> + <path id="apu" d="M 972 1256 C 968 1260 960 1267 960 1272 C 960 1277 967 1281 969 1283 C 969 1283 991 1300 991 1300 C 996 1304 1005 1314 1008 1320 C 1011 1326 1010 1330 1013 1334 C 1015 1337 1028 1344 1032 1345 C 1034 1342 1038 1334 1041 1332 C 1042 1330 1043 1329 1046 1330 C 1046 1330 1052 1335 1052 1335 C 1057 1339 1063 1343 1069 1346 C 1072 1347 1077 1351 1079 1348 C 1081 1345 1079 1339 1077 1336 C 1074 1331 1061 1323 1055 1319 C 1044 1312 1029 1303 1021 1294 C 1019 1292 1018 1290 1017 1287 C 1017 1285 1018 1282 1016 1280 C 1015 1279 1010 1279 1008 1279 C 1003 1278 999 1277 995 1275 C 984 1270 980 1262 972 1256 z"/> + <path id="nap" d="M 1029 1348 C 1024 1345 1013 1339 1009 1335 C 1006 1331 1007 1326 1005 1321 C 1002 1314 992 1305 986 1300 C 982 1297 978 1294 973 1296 C 969 1297 962 1303 962 1307 C 962 1310 964 1314 966 1316 C 966 1318 968 1321 969 1322 C 972 1326 979 1322 984 1326 C 988 1329 986 1333 991 1340 C 996 1346 1004 1350 1008 1357 C 1010 1360 1017 1377 1017 1381 C 1018 1383 1018 1385 1017 1387 C 1017 1389 1009 1402 1008 1404 C 1005 1407 1001 1412 1004 1417 C 1007 1421 1012 1419 1016 1416 C 1019 1413 1024 1405 1025 1400 C 1026 1397 1027 1392 1029 1389 C 1032 1385 1037 1387 1039 1385 C 1042 1383 1042 1378 1042 1375 C 1041 1362 1030 1364 1028 1356 C 1027 1354 1028 1350 1029 1348 z"/> + <path id="bur" d="M 773 975 C 769 985 759 1008 751 1015 C 747 1018 745 1018 742 1021 C 742 1021 733 1028 733 1028 C 733 1028 725 1033 725 1033 C 721 1036 720 1039 717 1041 C 713 1044 710 1043 707 1046 C 702 1052 703 1058 701 1064 C 699 1073 692 1083 683 1086 C 683 1090 684 1093 688 1096 C 692 1099 699 1098 702 1103 C 704 1107 701 1113 700 1118 C 700 1128 713 1145 724 1138 C 731 1134 735 1120 737 1112 C 737 1108 736 1100 740 1098 C 743 1096 746 1097 749 1098 C 754 1100 759 1101 764 1101 C 766 1101 770 1101 772 1100 C 772 1100 785 1086 785 1086 C 791 1080 807 1065 811 1058 C 813 1054 813 1049 815 1044 C 815 1044 818 1034 818 1034 C 818 1028 812 1019 808 1014 C 802 1003 800 998 802 986 C 802 986 773 975 773 975 z"/> + <path id="mar" d="M 741 1100 C 741 1112 739 1122 733 1133 C 730 1138 727 1142 721 1143 C 715 1144 712 1141 707 1139 C 704 1143 700 1147 695 1148 C 691 1150 682 1148 677 1153 C 674 1155 670 1164 667 1168 C 667 1168 650 1192 650 1192 C 654 1197 668 1205 674 1208 C 677 1209 682 1211 685 1211 C 690 1210 686 1204 690 1196 C 693 1189 703 1182 711 1181 C 721 1180 725 1187 733 1189 C 738 1190 743 1188 748 1191 C 754 1196 756 1207 768 1208 C 781 1210 791 1196 803 1193 C 801 1186 797 1185 797 1176 C 797 1176 798 1161 798 1161 C 797 1157 794 1153 794 1147 C 794 1140 800 1137 799 1133 C 799 1130 795 1127 794 1123 C 794 1117 800 1112 798 1108 C 797 1105 792 1106 789 1107 C 781 1109 778 1112 773 1104 C 760 1107 753 1102 741 1100 z"/> + <path id="gas" d="M 611 1058 C 611 1058 611 1079 611 1079 C 611 1079 614 1086 614 1086 C 614 1086 609 1084 609 1084 C 606 1088 601 1097 601 1102 C 600 1105 601 1107 601 1110 C 600 1113 591 1131 589 1134 C 587 1138 584 1141 580 1145 C 578 1146 576 1148 576 1151 C 577 1155 588 1165 592 1168 C 600 1173 611 1180 620 1183 C 620 1183 633 1185 633 1185 C 637 1186 643 1189 647 1191 C 647 1191 664 1167 664 1167 C 667 1162 671 1153 676 1149 C 682 1145 688 1147 695 1145 C 699 1143 702 1140 705 1137 C 702 1132 698 1128 697 1122 C 696 1116 699 1110 700 1104 C 695 1102 688 1101 684 1098 C 680 1094 681 1090 677 1087 C 675 1086 670 1085 663 1080 C 658 1076 655 1070 650 1067 C 643 1063 620 1059 611 1058 z"/> + <path id="pic" d="M 711 936 C 707 940 703 944 698 946 C 694 947 692 947 688 947 C 684 948 680 950 677 952 C 677 952 677 954 677 954 C 677 954 681 956 681 956 C 681 956 681 958 681 958 C 673 960 670 959 667 960 C 664 961 662 975 661 979 C 673 979 687 980 698 977 C 705 976 709 972 715 972 C 721 971 728 975 733 977 C 749 983 743 978 763 989 C 763 989 770 972 770 972 C 759 967 755 962 745 956 C 745 956 711 936 711 936 z"/> + <path id="par" d="M 661 980 C 657 990 660 1003 660 1014 C 660 1035 658 1046 654 1066 C 658 1069 658 1071 661 1074 C 666 1079 675 1084 682 1082 C 690 1081 695 1072 697 1065 C 699 1058 698 1052 703 1045 C 708 1039 711 1041 716 1038 C 716 1038 722 1031 722 1031 C 722 1031 732 1025 732 1025 C 732 1025 740 1018 740 1018 C 744 1015 746 1015 750 1012 C 753 1008 759 997 761 992 C 745 983 747 986 733 981 C 728 979 720 975 715 975 C 709 976 706 979 701 980 C 693 983 684 982 676 982 C 671 982 665 983 661 980 z"/> + <path id="bre" d="M 631 936 C 631 936 633 952 633 952 C 633 952 631 966 631 966 C 631 966 633 976 633 976 C 626 975 627 974 621 971 C 621 971 601 963 601 963 C 597 961 595 957 592 956 C 589 955 586 956 582 956 C 576 955 564 953 559 958 C 557 959 556 962 555 965 C 553 967 550 969 550 973 C 552 980 562 979 567 982 C 573 985 586 997 591 1002 C 595 1007 593 1008 595 1011 C 596 1013 598 1015 599 1019 C 601 1024 599 1027 599 1031 C 600 1037 605 1048 610 1052 C 613 1055 619 1056 623 1056 C 623 1056 651 1063 651 1063 C 653 1053 656 1036 656 1026 C 656 1026 656 1009 656 1009 C 656 1009 655 999 655 999 C 655 982 659 974 662 958 C 656 956 651 954 647 950 C 642 945 644 941 641 939 C 640 937 634 936 631 936 z M 553 968 C 553 968 553 969 553 969 C 553 969 552 968 552 968 C 552 968 553 968 553 968 z"/> + <path id="spa" d="M 386 1138 C 391 1137 398 1136 403 1138 C 407 1141 408 1144 414 1146 C 424 1149 439 1146 447 1159 C 449 1162 450 1166 450 1169 C 448 1177 442 1174 434 1184 C 434 1184 415 1216 415 1216 C 413 1219 410 1224 407 1226 C 405 1228 401 1230 400 1232 C 399 1235 399 1243 399 1246 C 399 1257 395 1257 393 1265 C 392 1272 396 1276 391 1284 C 388 1289 383 1291 379 1295 C 375 1300 372 1308 370 1314 C 379 1319 387 1329 389 1339 C 391 1347 388 1357 397 1362 C 404 1366 407 1361 413 1359 C 418 1358 420 1361 427 1361 C 427 1361 445 1357 445 1357 C 454 1357 462 1363 470 1365 C 478 1368 478 1367 485 1367 C 491 1366 497 1370 501 1369 C 507 1368 509 1360 520 1355 C 527 1352 541 1353 545 1350 C 545 1350 565 1326 565 1326 C 569 1323 575 1322 579 1321 C 577 1315 572 1310 572 1303 C 573 1298 577 1294 580 1290 C 584 1285 588 1276 592 1271 C 592 1271 599 1266 599 1266 C 604 1260 605 1257 613 1257 C 613 1255 613 1253 615 1251 C 618 1247 631 1245 636 1245 C 650 1243 675 1237 683 1225 C 685 1222 686 1220 686 1217 C 671 1217 656 1204 643 1196 C 640 1195 635 1192 632 1191 C 628 1191 626 1192 621 1190 C 613 1188 604 1183 597 1179 C 591 1175 583 1169 578 1164 C 578 1164 567 1151 567 1151 C 567 1151 558 1148 558 1148 C 553 1145 552 1143 548 1141 C 544 1139 540 1141 533 1138 C 533 1138 522 1132 522 1132 C 516 1130 514 1131 506 1127 C 506 1127 472 1110 472 1110 C 472 1110 448 1101 448 1101 C 439 1095 439 1088 427 1089 C 417 1089 419 1094 414 1096 C 409 1098 403 1095 400 1095 C 398 1095 396 1096 394 1098 C 385 1105 390 1114 390 1123 C 390 1129 388 1133 386 1138 z"/> + <path id="spa-sc" d="M 391 1284 C 388 1289 383 1291 379 1295 C 375 1300 372 1308 370 1314 C 379 1319 387 1329 389 1339 C 391 1347 388 1357 397 1362 C 404 1366 407 1361 413 1359 C 418 1358 420 1361 427 1361 C 427 1361 445 1357 445 1357 C 454 1357 462 1363 470 1365 C 478 1368 478 1367 485 1367 C 491 1366 497 1370 501 1369 C 507 1368 509 1360 520 1355 C 527 1352 541 1353 545 1350 C 545 1350 565 1326 565 1326 C 569 1323 575 1322 579 1321 C 577 1315 572 1310 572 1303 C 573 1298 577 1294 580 1290 C 584 1285 588 1276 592 1271 C 592 1271 599 1266 599 1266 C 604 1260 605 1257 613 1257 C 613 1255 613 1253 615 1251 C 618 1247 631 1245 636 1245 C 650 1243 675 1237 683 1225 C 685 1222 686 1220 686 1217 C 671 1217 656 1204 643 1196 C 640 1195 635 1192 632 1191 C 628 1191 626 1192 621 1190 C 613 1188 396 1276 391 1284 z"/> + <path id="spa-nc" d="M 386 1138 C 391 1137 398 1136 403 1138 C 407 1141 408 1144 414 1146 C 424 1149 439 1146 447 1159 C 449 1162 450 1166 450 1169 C 448 1177 442 1174 434 1184 C 434 1184 567 1151 567 1151 C 567 1151 558 1148 558 1148 C 553 1145 552 1143 548 1141 C 544 1139 540 1141 533 1138 C 533 1138 522 1132 522 1132 C 516 1130 514 1131 506 1127 C 506 1127 472 1110 472 1110 C 472 1110 448 1101 448 1101 C 439 1095 439 1088 427 1089 C 417 1089 419 1094 414 1096 C 409 1098 403 1095 400 1095 C 398 1095 396 1096 394 1098 C 385 1105 390 1114 390 1123 C 390 1129 388 1133 386 1138 z"/> + <path id="por" d="M 394 1143 C 391 1144 387 1144 385 1146 C 383 1148 381 1158 380 1161 C 378 1169 372 1180 369 1187 C 365 1194 356 1210 351 1215 C 346 1220 340 1218 336 1224 C 335 1227 332 1240 332 1243 C 332 1245 332 1248 333 1249 C 335 1253 343 1251 343 1259 C 343 1264 340 1269 338 1273 C 336 1280 339 1280 334 1288 C 332 1291 326 1298 327 1302 C 327 1304 337 1309 340 1310 C 344 1312 347 1314 352 1314 C 354 1314 362 1312 363 1311 C 365 1310 369 1298 371 1295 C 379 1282 387 1287 387 1273 C 387 1269 387 1265 388 1261 C 389 1256 392 1254 393 1248 C 394 1242 391 1236 394 1230 C 397 1223 401 1226 409 1215 C 409 1215 429 1180 429 1180 C 439 1168 446 1171 442 1163 C 437 1153 421 1156 412 1152 C 404 1149 404 1143 394 1143 z"/> + <path id="naf" d="M 216 1527 C 216 1527 780 1527 780 1527 C 780 1505 779 1473 783 1452 C 786 1442 790 1439 793 1431 C 784 1430 782 1426 770 1425 C 763 1424 763 1428 757 1427 C 752 1427 746 1422 739 1422 C 736 1422 721 1426 718 1427 C 715 1428 711 1430 708 1429 C 705 1429 701 1425 699 1423 C 695 1420 691 1417 686 1416 C 673 1412 646 1412 632 1412 C 632 1412 612 1410 612 1410 C 602 1410 593 1409 583 1411 C 575 1412 568 1415 561 1418 C 558 1420 552 1423 548 1423 C 544 1423 542 1420 539 1420 C 537 1419 526 1419 524 1420 C 520 1423 516 1427 510 1430 C 501 1434 486 1430 477 1427 C 474 1426 469 1424 466 1422 C 464 1419 463 1417 462 1414 C 457 1415 456 1416 451 1416 C 440 1416 423 1415 416 1406 C 411 1400 409 1391 406 1384 C 405 1381 403 1377 399 1378 C 392 1378 385 1387 381 1392 C 369 1405 360 1419 345 1428 C 332 1437 308 1433 292 1438 C 283 1441 272 1448 265 1454 C 265 1454 256 1465 256 1465 C 256 1465 242 1475 242 1475 C 232 1485 218 1513 216 1527 z"/> + <path id="tun" d="M 787 1527 C 787 1527 857 1527 857 1527 C 859 1527 863 1527 865 1526 C 867 1524 868 1521 869 1518 C 871 1510 872 1502 868 1494 C 864 1487 861 1486 856 1481 C 853 1478 852 1474 853 1470 C 854 1459 864 1455 869 1450 C 871 1448 873 1442 871 1440 C 869 1438 867 1439 865 1440 C 862 1442 855 1447 851 1446 C 846 1444 849 1438 846 1434 C 842 1429 832 1427 826 1427 C 826 1427 810 1430 810 1430 C 807 1431 804 1431 801 1433 C 795 1438 790 1451 789 1459 C 786 1477 787 1508 787 1527 z"/> + <path id="lon" d="M 716 901 C 713 899 704 896 702 893 C 699 888 709 883 712 880 C 721 875 735 868 736 856 C 736 854 736 851 735 849 C 731 842 713 838 705 840 C 705 840 694 845 694 845 C 689 847 684 849 679 851 C 679 851 669 853 669 853 C 661 856 655 866 655 875 C 656 883 659 891 664 898 C 665 900 667 903 669 905 C 672 907 677 907 680 908 C 693 910 705 910 716 901 z"/> + <path id="wal" d="M 612 810 C 624 811 622 824 617 828 C 612 833 594 831 590 837 C 586 844 597 847 601 849 C 608 852 613 860 619 864 C 625 866 626 863 634 865 C 633 867 632 868 630 870 C 619 878 611 870 603 870 C 596 870 596 877 590 872 C 583 878 586 880 571 888 C 565 892 560 890 555 896 C 558 897 561 900 564 900 C 567 900 572 897 575 896 C 582 894 591 897 596 902 C 603 899 605 894 610 892 C 615 891 624 896 630 897 C 630 897 660 900 660 900 C 657 891 650 882 651 872 C 652 864 657 856 664 852 C 668 849 671 849 672 847 C 674 845 672 836 672 832 C 667 831 649 824 645 821 C 642 818 640 815 640 810 C 640 807 640 803 639 800 C 633 792 615 804 612 810 z"/> + <path id="lvp" d="M 622 715 C 622 715 633 711 633 711 C 636 700 641 699 643 706 C 644 715 635 723 630 730 C 624 738 625 742 634 745 C 637 747 636 746 639 746 C 643 747 642 749 645 750 C 647 751 652 751 654 751 C 654 751 647 757 647 757 C 647 757 651 765 651 765 C 651 765 651 778 651 778 C 652 783 654 788 652 793 C 650 803 640 801 644 813 C 644 815 645 817 647 818 C 650 820 668 827 672 828 C 672 813 670 810 672 793 C 672 793 677 777 677 777 C 679 771 678 760 677 754 C 676 748 665 735 661 728 C 654 714 657 711 657 697 C 650 695 633 692 626 692 C 622 693 617 695 616 699 C 615 703 619 703 621 705 C 622 707 622 712 622 715 z"/> + <path id="yor" d="M 680 750 C 680 750 681 758 681 758 C 681 758 681 767 681 767 C 681 767 675 804 675 804 C 675 804 677 848 677 848 C 683 846 698 840 703 837 C 703 837 707 832 707 832 C 710 830 714 828 714 824 C 714 818 707 811 704 806 C 711 803 712 802 711 794 C 709 779 702 779 698 768 C 694 761 695 754 692 752 C 690 750 683 750 680 750 z"/> + <path id="edi" d="M 690 621 C 683 631 677 631 673 638 C 669 644 667 656 666 663 C 666 663 661 699 661 699 C 660 708 659 714 662 723 C 664 727 674 742 677 746 C 683 746 689 748 692 747 C 697 745 697 737 696 733 C 693 724 681 713 687 701 C 691 693 701 684 707 677 C 710 674 714 669 714 665 C 712 657 697 653 690 652 C 687 652 679 654 677 651 C 674 648 681 643 683 642 C 683 642 700 631 700 631 C 706 626 702 623 696 622 C 696 622 690 621 690 621 z"/> + <path id="cly" d="M 624 681 C 629 681 639 680 631 688 C 631 688 658 694 658 694 C 658 694 661 670 661 670 C 662 661 665 645 669 637 C 674 629 682 627 686 620 C 682 619 676 617 672 618 C 667 619 664 624 661 628 C 659 630 647 638 645 639 C 640 641 636 632 631 639 C 627 646 636 652 636 658 C 635 664 626 673 624 681 z"/> + <path id="nat" d="M 202 175 C 202 175 202 859 202 859 C 202 859 240 849 240 849 C 272 844 323 842 355 846 C 372 849 395 856 410 862 C 416 865 419 866 424 869 C 426 871 429 873 432 872 C 436 871 445 861 448 858 C 448 858 477 829 477 829 C 483 823 489 819 491 811 C 487 809 487 808 488 804 C 483 799 483 797 489 794 C 488 792 485 788 488 786 C 491 782 496 789 500 786 C 500 786 508 774 508 774 C 515 766 523 765 533 765 C 527 762 510 754 512 746 C 513 741 519 738 521 733 C 523 727 517 721 525 718 C 530 716 537 719 542 722 C 549 725 553 726 560 724 C 559 722 557 719 558 717 C 558 714 561 713 563 712 C 568 708 571 703 576 702 C 580 702 591 705 594 707 C 594 707 599 711 599 711 C 603 714 606 712 610 716 C 613 720 612 725 613 729 C 614 736 616 737 617 741 C 617 745 613 750 611 753 C 615 756 618 759 623 761 C 631 764 640 763 648 763 C 645 759 645 759 647 754 C 643 752 642 752 641 748 C 640 748 638 749 637 749 C 632 749 623 744 624 737 C 626 731 633 724 637 719 C 640 713 640 709 640 703 C 634 707 637 713 628 713 C 627 714 627 715 626 716 C 625 716 624 717 622 717 C 618 715 620 708 620 705 C 613 705 611 700 615 695 C 620 689 627 691 633 683 C 633 683 629 683 629 683 C 612 683 633 668 633 658 C 634 652 625 646 629 639 C 633 632 638 633 642 638 C 646 636 656 630 659 627 C 664 621 665 618 673 615 C 673 615 670 614 670 614 C 670 614 672 594 672 594 C 672 594 676 553 676 553 C 676 553 684 485 684 485 C 684 476 684 469 682 460 C 679 450 664 424 658 413 C 658 413 629 359 629 359 C 629 359 620 362 620 362 C 620 362 593 362 593 362 C 593 362 572 360 572 360 C 563 361 553 365 544 359 C 534 353 531 337 526 330 C 522 325 515 322 510 319 C 508 318 504 316 503 313 C 503 312 504 311 504 310 C 507 299 511 311 517 309 C 523 307 523 303 525 298 C 525 298 526 293 526 293 C 527 286 519 284 514 279 C 512 277 509 274 511 272 C 514 270 518 272 520 272 C 527 275 531 280 539 282 C 537 275 539 275 545 273 C 542 260 529 267 522 258 C 518 253 519 244 528 251 C 531 247 532 243 536 248 C 538 246 540 241 543 240 C 550 237 553 249 553 254 C 559 242 556 229 556 216 C 556 216 556 175 556 175 C 556 175 202 175 202 175 z"/> + <path id="nrg" d="M 560 175 C 560 175 560 228 560 228 C 560 243 569 238 568 255 C 567 263 563 265 560 269 C 558 273 556 278 556 282 C 556 282 568 277 568 277 C 568 277 568 281 568 281 C 577 279 575 273 580 268 C 586 263 587 268 587 273 C 587 273 587 278 587 278 C 589 277 593 276 595 276 C 601 276 602 283 604 287 C 606 286 609 285 611 285 C 613 285 614 287 618 288 C 618 288 630 288 630 288 C 633 288 635 288 638 289 C 653 293 651 308 652 320 C 653 327 654 334 651 341 C 647 348 639 353 632 357 C 632 357 667 423 667 423 C 679 443 688 458 688 482 C 688 482 684 516 684 516 C 684 516 677 587 677 587 C 677 587 674 615 674 615 C 674 615 698 620 698 620 C 701 621 705 623 708 623 C 710 622 718 615 720 612 C 730 604 737 597 748 590 C 775 574 808 568 838 562 C 848 559 868 554 877 555 C 886 543 890 538 905 541 C 901 531 909 531 917 530 C 913 525 913 522 920 519 C 926 517 928 521 932 520 C 935 519 941 513 947 510 C 953 506 957 508 959 506 C 961 504 961 502 964 497 C 967 492 978 481 983 480 C 985 479 987 480 989 480 C 989 466 999 454 1006 442 C 1012 433 1017 421 1023 413 C 1023 413 1042 394 1042 394 C 1042 394 1053 378 1053 378 C 1053 378 1069 356 1069 356 C 1069 356 1080 342 1080 342 C 1082 340 1086 336 1087 335 C 1088 332 1085 330 1085 327 C 1084 324 1088 319 1090 316 C 1094 313 1097 315 1099 312 C 1102 310 1103 305 1109 303 C 1113 301 1120 302 1131 297 C 1140 293 1145 286 1153 281 C 1159 277 1169 272 1172 266 C 1174 263 1172 256 1171 252 C 1171 252 1171 234 1171 234 C 1171 234 1172 219 1172 219 C 1172 219 1174 175 1174 175 C 1174 175 560 175 560 175 z"/> + <path id="bar" d="M 1178 175 C 1178 175 1176 214 1176 214 C 1176 214 1175 230 1175 230 C 1175 230 1175 246 1175 246 C 1175 249 1175 259 1177 261 C 1179 264 1183 263 1184 267 C 1186 271 1183 283 1183 288 C 1189 282 1189 273 1193 266 C 1194 264 1197 260 1199 262 C 1201 265 1199 274 1200 279 C 1204 273 1202 263 1207 258 C 1210 254 1217 257 1218 262 C 1218 267 1215 270 1217 278 C 1222 274 1222 268 1228 266 C 1234 265 1252 272 1249 280 C 1247 286 1236 289 1230 289 C 1232 292 1237 296 1241 297 C 1246 297 1248 293 1260 293 C 1265 285 1277 288 1278 293 C 1278 295 1275 299 1274 301 C 1284 305 1293 302 1303 302 C 1306 302 1312 302 1315 302 C 1322 304 1337 311 1344 314 C 1344 314 1359 319 1359 319 C 1371 322 1386 326 1395 334 C 1398 337 1407 349 1408 353 C 1410 365 1401 379 1393 387 C 1390 390 1387 393 1383 395 C 1379 396 1370 397 1366 397 C 1357 397 1330 394 1322 392 C 1322 392 1313 388 1313 388 C 1309 388 1306 390 1301 388 C 1295 386 1294 383 1287 380 C 1289 391 1296 392 1304 398 C 1308 401 1310 404 1315 406 C 1319 408 1323 409 1327 412 C 1334 419 1331 430 1333 436 C 1335 440 1340 447 1343 451 C 1345 455 1346 459 1350 462 C 1350 462 1369 469 1369 469 C 1372 470 1384 476 1387 475 C 1389 475 1392 472 1394 470 C 1392 465 1389 460 1384 458 C 1381 458 1378 458 1375 457 C 1371 455 1367 450 1365 446 C 1363 441 1364 431 1371 430 C 1375 430 1380 434 1383 436 C 1388 438 1391 439 1397 439 C 1397 439 1423 439 1423 439 C 1430 439 1441 442 1447 441 C 1447 441 1447 439 1447 439 C 1440 437 1430 437 1426 435 C 1420 432 1418 426 1413 422 C 1409 418 1402 415 1401 410 C 1399 404 1408 395 1411 391 C 1420 380 1419 376 1422 372 C 1428 366 1440 366 1448 367 C 1451 360 1452 347 1447 340 C 1443 336 1439 337 1436 331 C 1433 326 1434 317 1430 306 C 1426 297 1421 294 1416 287 C 1428 283 1455 284 1459 299 C 1461 306 1452 309 1450 316 C 1450 318 1450 321 1452 322 C 1456 327 1471 331 1477 329 C 1493 325 1488 312 1489 300 C 1491 290 1496 278 1502 269 C 1504 265 1511 254 1515 253 C 1517 253 1518 253 1520 254 C 1520 249 1518 242 1520 237 C 1526 227 1534 237 1535 244 C 1537 251 1532 257 1532 270 C 1532 285 1533 289 1542 302 C 1543 298 1537 288 1537 282 C 1537 276 1539 270 1541 264 C 1541 260 1542 254 1544 251 C 1544 251 1548 244 1548 244 C 1550 240 1549 236 1554 229 C 1560 222 1565 224 1570 219 C 1574 214 1575 208 1577 204 C 1578 202 1580 199 1583 199 C 1585 199 1587 203 1588 204 C 1591 207 1593 209 1597 210 C 1596 207 1595 203 1595 200 C 1595 194 1598 192 1594 186 C 1593 184 1591 182 1589 181 C 1587 179 1584 177 1582 176 C 1578 174 1566 175 1561 175 C 1561 175 1493 175 1493 175 C 1493 175 1178 175 1178 175 z"/> + <path id="bot" d="M 1058 692 C 1058 692 1062 700 1062 700 C 1062 700 1064 709 1064 709 C 1064 709 1085 715 1085 715 C 1085 715 1129 730 1129 730 C 1129 730 1154 733 1154 733 C 1155 731 1156 729 1158 727 C 1167 716 1178 731 1185 738 C 1187 741 1189 744 1192 745 C 1202 747 1204 731 1204 724 C 1204 719 1205 707 1202 703 C 1200 700 1197 701 1192 697 C 1186 692 1184 681 1186 673 C 1188 666 1194 663 1200 660 C 1211 654 1223 653 1236 654 C 1236 654 1250 656 1250 656 C 1264 657 1263 645 1270 640 C 1273 638 1275 638 1278 637 C 1278 637 1286 633 1286 633 C 1289 633 1294 634 1298 634 C 1287 619 1270 633 1268 612 C 1268 612 1261 617 1261 617 C 1261 617 1237 625 1237 625 C 1237 625 1208 636 1208 636 C 1203 638 1185 643 1181 642 C 1176 642 1175 637 1170 635 C 1170 635 1163 633 1163 633 C 1163 633 1154 628 1154 628 C 1154 628 1147 625 1147 625 C 1142 622 1139 614 1140 608 C 1140 599 1144 597 1144 587 C 1144 587 1138 553 1138 553 C 1137 542 1147 531 1154 523 C 1154 523 1166 508 1166 508 C 1166 508 1186 478 1186 478 C 1186 478 1197 464 1197 464 C 1198 462 1198 459 1197 457 C 1194 447 1182 441 1172 440 C 1168 440 1163 441 1159 443 C 1150 449 1142 466 1142 477 C 1142 487 1145 488 1143 494 C 1140 504 1125 518 1117 525 C 1117 525 1103 535 1103 535 C 1100 538 1097 542 1095 545 C 1085 558 1083 563 1079 579 C 1077 590 1074 609 1079 619 C 1082 624 1086 625 1090 629 C 1097 637 1103 646 1100 657 C 1100 661 1097 666 1095 669 C 1085 686 1076 689 1058 692 z"/> + <path id="bal" d="M 1060 713 C 1059 721 1057 725 1057 734 C 1057 744 1056 753 1052 763 C 1050 769 1047 776 1041 779 C 1037 781 1031 780 1026 780 C 1021 780 1016 781 1012 785 C 1006 792 1007 803 993 804 C 990 804 985 803 983 804 C 981 805 975 814 973 817 C 966 825 955 830 945 829 C 931 828 931 808 917 804 C 918 812 919 816 924 823 C 927 826 935 831 935 836 C 935 839 932 844 930 846 C 934 846 939 846 942 845 C 946 844 947 842 952 840 C 952 840 967 836 967 836 C 971 834 979 830 984 831 C 985 831 986 831 987 831 C 994 835 986 840 989 844 C 991 848 1002 850 1007 850 C 1024 850 1040 843 1056 836 C 1070 830 1079 824 1094 831 C 1090 839 1098 844 1107 840 C 1113 838 1118 835 1121 830 C 1123 827 1123 824 1125 821 C 1127 818 1130 817 1133 816 C 1135 816 1137 817 1139 816 C 1143 814 1144 805 1144 801 C 1146 790 1140 777 1143 762 C 1143 762 1152 735 1152 735 C 1144 735 1138 735 1130 734 C 1122 732 1115 729 1108 727 C 1097 723 1071 715 1060 713 z"/> + <path id="ska" d="M 888 693 C 886 712 885 718 894 735 C 894 735 896 735 896 735 C 902 728 907 729 915 726 C 921 724 926 722 931 718 C 934 717 939 713 942 717 C 944 719 944 721 944 723 C 945 727 944 731 942 735 C 940 739 936 743 936 748 C 937 752 940 754 943 757 C 946 761 945 763 948 765 C 949 766 954 767 956 767 C 956 767 976 771 976 771 C 977 768 980 758 980 756 C 980 751 972 733 970 727 C 970 727 962 698 962 698 C 961 692 958 685 958 679 C 959 674 960 664 954 661 C 950 660 947 665 938 670 C 930 674 928 674 919 679 C 908 686 902 692 888 693 z"/> + <path id="hel" d="M 817 848 C 830 846 840 850 849 848 C 856 846 859 840 865 840 C 873 840 877 851 884 847 C 889 844 890 837 890 832 C 890 832 890 819 890 819 C 890 814 889 813 889 807 C 889 801 891 798 891 790 C 891 785 889 784 888 780 C 887 777 887 772 887 769 C 887 769 867 769 867 769 C 853 769 842 778 834 788 C 823 802 821 815 819 832 C 819 832 817 848 817 848 z"/> + <path id="nth" d="M 678 650 C 678 650 687 650 687 650 C 693 650 695 650 700 652 C 703 653 705 654 708 655 C 718 662 717 668 711 676 C 704 684 689 696 688 706 C 687 715 695 726 698 734 C 700 741 697 744 697 751 C 697 758 697 764 701 770 C 705 778 709 779 712 789 C 715 797 716 804 707 807 C 710 812 719 820 716 827 C 713 831 710 831 707 837 C 707 837 723 839 723 839 C 726 840 729 841 732 843 C 734 844 735 845 736 847 C 747 862 724 875 714 882 C 712 883 705 887 704 889 C 702 895 714 895 717 900 C 718 902 718 909 724 912 C 726 913 735 911 738 910 C 748 909 771 905 778 899 C 783 894 787 881 790 875 C 796 863 802 857 814 851 C 812 844 814 838 815 831 C 818 807 827 780 851 769 C 862 763 876 765 888 765 C 888 765 892 740 892 740 C 892 737 888 731 886 728 C 883 722 883 718 883 711 C 883 711 883 693 883 693 C 882 691 877 689 875 687 C 870 684 860 676 860 669 C 859 658 875 655 877 645 C 874 644 872 644 869 644 C 867 645 863 646 861 645 C 856 644 857 637 860 634 C 865 630 868 634 872 627 C 872 627 861 623 861 623 C 864 617 867 619 873 617 C 868 608 864 611 863 602 C 863 594 867 577 870 569 C 870 569 874 558 874 558 C 874 558 829 567 829 567 C 803 573 776 579 752 592 C 735 602 727 611 713 623 C 706 629 703 632 695 637 C 689 641 681 643 678 650 z"/> + <path id="eng" d="M 498 922 C 498 922 514 932 514 932 C 514 932 541 946 541 946 C 541 946 558 954 558 954 C 558 954 571 952 571 952 C 571 952 583 954 583 954 C 583 954 592 954 592 954 C 592 954 602 962 602 962 C 602 962 610 964 610 964 C 610 964 630 973 630 973 C 627 962 629 965 631 956 C 631 951 627 939 629 936 C 631 933 640 935 643 936 C 646 940 645 945 648 949 C 654 956 668 957 677 957 C 674 951 676 950 682 947 C 691 943 693 947 700 943 C 712 936 714 925 721 915 C 719 913 716 908 714 907 C 712 907 708 908 706 909 C 702 910 698 911 693 911 C 688 911 675 910 671 908 C 666 906 666 904 659 903 C 659 903 630 900 630 900 C 625 899 618 895 613 894 C 606 894 603 902 598 903 C 596 904 593 901 591 900 C 588 899 583 897 579 897 C 574 898 570 901 566 902 C 560 903 559 899 554 900 C 554 900 534 907 534 907 C 534 907 498 922 498 922 z"/> + <path id="iri" d="M 490 820 C 490 820 452 859 452 859 C 447 864 439 871 436 877 C 436 877 475 907 475 907 C 479 910 489 918 493 919 C 497 919 511 913 515 911 C 515 911 549 897 549 897 C 554 895 555 892 558 890 C 561 888 563 890 570 886 C 574 884 580 881 583 878 C 585 875 586 872 588 871 C 591 869 592 871 593 873 C 596 871 599 868 602 868 C 604 867 611 870 614 870 C 622 872 625 871 631 867 C 627 866 623 868 619 866 C 613 863 608 854 600 850 C 595 849 584 845 587 838 C 593 825 620 836 618 818 C 617 811 612 813 610 810 C 608 807 617 802 619 801 C 631 793 634 792 645 802 C 647 801 648 800 649 798 C 652 791 648 774 648 766 C 632 770 618 765 607 753 C 603 754 599 755 596 759 C 585 769 591 789 583 799 C 579 803 571 812 567 814 C 561 817 554 814 547 814 C 538 814 535 816 526 817 C 518 819 498 821 490 820 z"/> + <path id="mid" d="M 202 1527 C 204 1527 210 1527 212 1526 C 214 1525 216 1518 217 1516 C 219 1509 222 1502 226 1496 C 230 1489 236 1479 242 1473 C 242 1473 254 1464 254 1464 C 260 1458 263 1453 270 1448 C 278 1442 288 1436 298 1434 C 298 1434 336 1430 336 1430 C 353 1425 369 1402 381 1389 C 381 1389 391 1379 391 1379 C 393 1378 396 1376 397 1374 C 401 1366 392 1362 389 1356 C 389 1356 387 1340 387 1340 C 385 1330 379 1321 370 1316 C 361 1311 358 1317 349 1316 C 343 1315 338 1311 333 1308 C 330 1307 325 1305 324 1302 C 323 1298 330 1291 332 1288 C 336 1281 334 1279 336 1273 C 337 1268 340 1265 340 1260 C 340 1252 335 1256 332 1252 C 328 1248 329 1242 331 1238 C 332 1232 335 1224 340 1220 C 344 1216 347 1217 352 1210 C 361 1200 375 1172 378 1159 C 378 1159 383 1139 383 1139 C 385 1134 387 1130 388 1123 C 388 1114 383 1107 390 1099 C 393 1096 397 1093 401 1093 C 404 1093 410 1096 414 1094 C 416 1094 416 1091 418 1090 C 421 1087 427 1086 430 1086 C 441 1087 439 1095 453 1101 C 453 1101 477 1109 477 1109 C 477 1109 503 1123 503 1123 C 503 1123 523 1130 523 1130 C 523 1130 534 1136 534 1136 C 539 1138 544 1138 547 1139 C 555 1141 558 1149 570 1148 C 575 1148 583 1139 587 1135 C 589 1132 598 1113 598 1110 C 599 1107 598 1106 598 1103 C 599 1096 605 1086 610 1082 C 606 1069 610 1069 609 1059 C 607 1048 598 1044 597 1032 C 597 1028 598 1025 597 1021 C 597 1016 594 1014 593 1012 C 592 1009 592 1007 591 1005 C 590 1003 586 1001 584 999 C 579 994 574 988 568 984 C 563 982 554 983 549 976 C 548 974 547 971 547 969 C 554 967 554 964 555 957 C 555 957 535 947 535 947 C 512 936 496 928 475 912 C 453 895 439 879 412 867 C 390 857 356 848 332 848 C 332 848 290 848 290 848 C 290 848 278 849 278 849 C 259 850 241 852 222 858 C 219 859 205 862 203 864 C 202 865 202 869 202 871 C 202 871 202 1527 202 1527 z"/> + <path id="wes" d="M 674 1309 C 673 1311 672 1313 671 1315 C 670 1316 668 1318 667 1319 C 658 1325 653 1312 645 1313 C 645 1313 624 1321 624 1321 C 610 1325 596 1324 582 1324 C 577 1324 568 1325 564 1329 C 564 1329 546 1351 546 1351 C 540 1356 526 1353 517 1359 C 510 1363 506 1371 500 1372 C 497 1372 493 1370 490 1369 C 484 1368 482 1371 475 1369 C 475 1369 452 1360 452 1360 C 440 1358 436 1362 427 1363 C 418 1364 418 1358 408 1364 C 407 1365 404 1366 403 1368 C 400 1372 404 1376 406 1379 C 410 1386 412 1399 418 1406 C 424 1412 442 1414 451 1414 C 454 1414 460 1412 462 1413 C 466 1414 464 1418 470 1422 C 477 1426 494 1429 502 1429 C 516 1429 517 1420 526 1417 C 528 1417 537 1417 539 1417 C 543 1419 544 1421 549 1420 C 549 1420 571 1412 571 1412 C 580 1409 591 1407 600 1407 C 621 1407 621 1410 634 1410 C 650 1410 670 1409 685 1413 C 691 1415 694 1417 699 1421 C 701 1423 705 1427 708 1427 C 711 1428 714 1425 717 1425 C 717 1425 739 1419 739 1419 C 744 1420 753 1424 757 1425 C 762 1426 763 1422 770 1422 C 780 1423 784 1427 791 1429 C 799 1430 807 1428 815 1427 C 815 1427 819 1396 819 1396 C 819 1389 816 1376 813 1370 C 811 1366 808 1363 807 1358 C 805 1353 809 1345 809 1338 C 809 1335 808 1325 807 1324 C 806 1322 801 1320 799 1320 C 799 1320 779 1313 779 1313 C 758 1306 746 1305 724 1305 C 724 1305 692 1308 692 1308 C 686 1309 680 1310 674 1309 z"/> + <path id="gol" d="M 674 1307 C 674 1307 698 1304 698 1304 C 720 1301 738 1299 760 1304 C 760 1304 809 1319 809 1319 C 813 1307 808 1308 806 1300 C 806 1296 807 1293 808 1290 C 821 1296 822 1286 836 1285 C 836 1269 826 1273 825 1257 C 825 1251 826 1242 831 1239 C 835 1236 838 1236 843 1232 C 848 1227 848 1222 854 1221 C 860 1219 870 1222 876 1223 C 876 1223 874 1210 874 1210 C 874 1206 874 1202 873 1198 C 872 1190 868 1189 862 1184 C 855 1179 852 1175 842 1177 C 832 1179 829 1185 822 1189 C 822 1189 799 1197 799 1197 C 790 1201 784 1208 774 1210 C 765 1212 758 1207 752 1200 C 750 1197 750 1195 748 1193 C 743 1190 739 1192 734 1191 C 728 1190 721 1184 715 1183 C 711 1183 706 1185 702 1186 C 682 1196 698 1217 680 1232 C 672 1238 656 1244 646 1246 C 640 1247 622 1249 618 1251 C 614 1254 616 1257 613 1259 C 611 1260 608 1260 605 1262 C 605 1262 600 1267 600 1267 C 600 1267 594 1273 594 1273 C 590 1276 586 1284 583 1289 C 583 1289 576 1298 576 1298 C 573 1304 576 1316 582 1319 C 584 1320 588 1320 590 1320 C 590 1320 606 1320 606 1320 C 619 1320 631 1316 642 1310 C 647 1307 648 1304 653 1301 C 661 1298 669 1301 674 1307 z"/> + <path id="tyn" d="M 816 1367 C 820 1377 822 1381 822 1392 C 822 1392 822 1404 822 1404 C 822 1404 819 1426 819 1426 C 829 1424 844 1425 849 1435 C 850 1438 850 1442 850 1445 C 856 1444 870 1435 876 1432 C 876 1432 906 1419 906 1419 C 911 1415 913 1407 919 1405 C 924 1403 938 1408 944 1409 C 944 1409 951 1409 951 1409 C 964 1409 970 1407 983 1409 C 992 1398 994 1403 1004 1406 C 1006 1403 1014 1390 1015 1387 C 1016 1382 1014 1376 1012 1371 C 1010 1367 1009 1360 1006 1357 C 1003 1352 996 1347 991 1342 C 989 1340 987 1338 986 1335 C 985 1333 985 1330 982 1328 C 980 1325 970 1326 966 1326 C 967 1319 963 1315 960 1309 C 958 1306 957 1302 954 1300 C 950 1298 942 1298 938 1297 C 932 1296 927 1292 923 1287 C 923 1287 913 1273 913 1273 C 913 1273 898 1255 898 1255 C 898 1255 885 1238 885 1238 C 883 1236 880 1229 878 1228 C 875 1226 869 1225 866 1225 C 862 1224 857 1222 853 1225 C 847 1229 850 1237 850 1243 C 850 1252 849 1258 845 1266 C 843 1270 840 1275 839 1279 C 838 1288 844 1287 847 1293 C 848 1297 848 1303 848 1307 C 848 1318 842 1335 839 1346 C 838 1350 836 1358 833 1360 C 830 1363 828 1361 825 1362 C 823 1363 818 1366 816 1367 z"/> + <path id="adr" d="M 1104 1335 C 1104 1321 1105 1317 1108 1304 C 1109 1300 1111 1292 1109 1288 C 1107 1284 1101 1280 1098 1277 C 1085 1265 1083 1264 1069 1254 C 1069 1254 1040 1231 1040 1231 C 1040 1231 1022 1219 1022 1219 C 1022 1219 1006 1205 1006 1205 C 1001 1201 995 1197 990 1192 C 986 1187 981 1178 982 1172 C 983 1164 989 1163 985 1155 C 978 1159 976 1173 969 1172 C 966 1171 962 1165 962 1162 C 960 1155 966 1148 964 1145 C 962 1142 958 1143 956 1144 C 952 1144 943 1148 939 1150 C 937 1151 935 1153 934 1155 C 933 1158 936 1161 935 1164 C 935 1166 932 1169 931 1171 C 929 1175 930 1182 931 1186 C 937 1198 952 1206 960 1219 C 964 1227 964 1234 967 1242 C 970 1251 979 1261 986 1267 C 991 1271 997 1274 1004 1276 C 1007 1276 1016 1277 1018 1279 C 1022 1283 1016 1286 1022 1293 C 1029 1301 1059 1319 1070 1327 C 1073 1329 1076 1332 1079 1335 C 1080 1337 1082 1340 1084 1340 C 1087 1341 1100 1336 1104 1335 z"/> + <path id="ion" d="M 1044 1331 C 1040 1336 1036 1342 1032 1348 C 1031 1350 1029 1353 1030 1356 C 1031 1359 1036 1361 1038 1363 C 1044 1369 1047 1377 1043 1385 C 1039 1391 1035 1386 1031 1390 C 1029 1392 1029 1397 1028 1400 C 1025 1406 1020 1416 1015 1419 C 1007 1425 999 1419 1001 1409 C 995 1406 990 1403 987 1411 C 990 1413 992 1414 993 1418 C 993 1422 990 1429 989 1434 C 989 1434 986 1455 986 1455 C 985 1458 982 1460 979 1461 C 969 1462 961 1453 954 1448 C 950 1444 937 1436 932 1433 C 932 1433 919 1429 919 1429 C 914 1427 912 1423 909 1423 C 906 1422 901 1425 898 1426 C 898 1426 874 1437 874 1437 C 877 1455 856 1455 855 1470 C 854 1481 866 1486 871 1495 C 876 1506 872 1516 868 1527 C 868 1527 1223 1527 1223 1527 C 1223 1527 1224 1509 1224 1509 C 1224 1509 1211 1479 1211 1479 C 1209 1474 1206 1464 1199 1464 C 1195 1464 1196 1471 1189 1475 C 1187 1467 1186 1458 1178 1454 C 1177 1456 1174 1462 1171 1461 C 1170 1460 1169 1457 1168 1455 C 1167 1450 1167 1444 1165 1439 C 1162 1431 1155 1427 1154 1421 C 1153 1412 1162 1412 1165 1411 C 1173 1409 1173 1407 1182 1408 C 1193 1410 1195 1414 1203 1416 C 1204 1408 1203 1409 1195 1405 C 1193 1404 1191 1403 1188 1402 C 1188 1402 1164 1406 1164 1406 C 1161 1406 1157 1407 1155 1406 C 1153 1405 1136 1392 1151 1387 C 1146 1384 1145 1387 1141 1385 C 1138 1384 1133 1379 1131 1376 C 1125 1369 1128 1367 1126 1361 C 1124 1355 1111 1342 1106 1340 C 1102 1338 1088 1343 1084 1346 C 1081 1348 1080 1351 1077 1351 C 1074 1352 1070 1349 1068 1348 C 1059 1344 1052 1337 1044 1331 z"/> + <path id="aeg" d="M 1227 1321 C 1227 1321 1242 1339 1242 1339 C 1237 1336 1231 1334 1225 1333 C 1225 1333 1233 1347 1233 1347 C 1233 1347 1217 1339 1217 1339 C 1217 1339 1221 1347 1221 1347 C 1214 1346 1202 1339 1201 1332 C 1200 1330 1201 1329 1201 1327 C 1184 1333 1203 1353 1209 1360 C 1211 1362 1214 1366 1214 1369 C 1213 1373 1207 1369 1204 1370 C 1202 1370 1201 1371 1199 1372 C 1200 1374 1202 1377 1202 1379 C 1202 1381 1200 1383 1200 1386 C 1202 1390 1213 1394 1217 1395 C 1214 1391 1213 1391 1210 1388 C 1204 1382 1209 1379 1213 1380 C 1215 1381 1219 1384 1221 1385 C 1227 1389 1231 1389 1234 1392 C 1238 1395 1238 1401 1233 1404 C 1233 1404 1233 1413 1233 1413 C 1233 1413 1234 1425 1234 1425 C 1229 1423 1225 1418 1219 1418 C 1217 1418 1213 1418 1213 1421 C 1213 1425 1224 1429 1218 1434 C 1213 1439 1210 1431 1200 1434 C 1200 1434 1209 1455 1209 1455 C 1209 1455 1212 1470 1212 1470 C 1212 1470 1223 1497 1223 1497 C 1225 1496 1228 1493 1231 1493 C 1234 1492 1239 1494 1242 1496 C 1247 1497 1252 1498 1257 1498 C 1267 1498 1268 1496 1279 1498 C 1279 1498 1296 1504 1296 1504 C 1298 1504 1300 1503 1302 1502 C 1305 1502 1306 1502 1309 1501 C 1318 1498 1333 1485 1339 1478 C 1345 1470 1350 1464 1350 1454 C 1340 1451 1353 1448 1352 1438 C 1352 1438 1335 1442 1335 1442 C 1333 1442 1330 1442 1329 1440 C 1328 1436 1332 1436 1329 1429 C 1328 1429 1326 1430 1325 1430 C 1316 1429 1322 1415 1318 1410 C 1314 1404 1302 1405 1305 1399 C 1306 1397 1309 1395 1309 1393 C 1310 1390 1308 1387 1309 1384 C 1309 1381 1311 1379 1311 1376 C 1311 1374 1308 1369 1306 1366 C 1305 1362 1304 1357 1303 1353 C 1297 1354 1293 1358 1289 1357 C 1286 1357 1285 1354 1284 1352 C 1281 1347 1276 1342 1280 1336 C 1284 1328 1294 1326 1300 1319 C 1300 1319 1288 1320 1288 1320 C 1288 1320 1280 1315 1280 1315 C 1280 1315 1259 1313 1259 1313 C 1259 1313 1238 1316 1238 1316 C 1238 1316 1227 1321 1227 1321 z"/> + <path id="eas" d="M 1304 1506 C 1296 1516 1288 1514 1277 1515 C 1272 1516 1268 1518 1263 1516 C 1258 1515 1255 1513 1245 1511 C 1245 1511 1227 1509 1227 1509 C 1227 1509 1227 1527 1227 1527 C 1227 1527 1582 1527 1582 1527 C 1582 1527 1582 1515 1582 1515 C 1582 1515 1579 1485 1579 1485 C 1579 1485 1576 1461 1576 1461 C 1576 1461 1571 1462 1571 1462 C 1571 1462 1571 1440 1571 1440 C 1571 1440 1567 1432 1567 1432 C 1567 1432 1572 1420 1572 1420 C 1572 1420 1572 1409 1572 1409 C 1564 1413 1565 1422 1558 1424 C 1551 1427 1543 1417 1534 1423 C 1530 1426 1524 1436 1519 1442 C 1512 1449 1495 1455 1485 1454 C 1478 1454 1473 1450 1467 1447 C 1457 1441 1448 1435 1436 1435 C 1434 1435 1431 1435 1430 1435 C 1419 1438 1424 1450 1419 1457 C 1417 1460 1407 1463 1403 1464 C 1397 1465 1390 1463 1385 1459 C 1385 1459 1378 1451 1378 1451 C 1376 1450 1371 1450 1368 1449 C 1365 1449 1362 1447 1359 1448 C 1353 1450 1355 1458 1350 1469 C 1342 1482 1332 1492 1319 1500 C 1314 1502 1310 1506 1304 1506 z M 1536 1460 C 1534 1467 1530 1468 1527 1474 C 1525 1479 1528 1482 1526 1485 C 1525 1487 1523 1487 1520 1490 C 1515 1493 1513 1498 1504 1502 C 1495 1507 1483 1508 1477 1498 C 1476 1496 1475 1493 1475 1491 C 1474 1485 1481 1487 1487 1482 C 1491 1479 1490 1477 1494 1475 C 1494 1475 1517 1470 1517 1470 C 1524 1467 1528 1462 1536 1460 z"/> + <path id="bla" d="M 1570 1032 C 1570 1032 1546 1047 1546 1047 C 1546 1047 1511 1064 1511 1064 C 1511 1064 1483 1087 1483 1087 C 1483 1087 1462 1097 1462 1097 C 1462 1097 1474 1104 1474 1104 C 1481 1109 1488 1117 1496 1118 C 1505 1119 1513 1102 1521 1115 C 1523 1119 1523 1123 1520 1126 C 1515 1130 1507 1128 1503 1130 C 1500 1132 1499 1135 1496 1137 C 1492 1140 1489 1140 1486 1142 C 1483 1144 1478 1151 1475 1154 C 1471 1158 1467 1161 1461 1159 C 1457 1156 1454 1153 1453 1148 C 1453 1145 1454 1140 1452 1137 C 1449 1133 1442 1133 1438 1131 C 1435 1130 1432 1128 1432 1125 C 1433 1120 1438 1115 1442 1112 C 1444 1110 1448 1109 1449 1106 C 1450 1103 1448 1101 1445 1101 C 1439 1101 1428 1108 1417 1105 C 1408 1102 1409 1097 1402 1094 C 1405 1089 1409 1089 1415 1088 C 1413 1087 1413 1086 1411 1085 C 1404 1084 1389 1089 1384 1093 C 1380 1097 1378 1102 1375 1104 C 1372 1105 1369 1103 1366 1102 C 1368 1105 1371 1109 1371 1112 C 1371 1116 1368 1120 1366 1123 C 1364 1127 1362 1133 1361 1138 C 1359 1145 1360 1152 1357 1158 C 1353 1166 1347 1166 1344 1176 C 1344 1176 1339 1210 1339 1210 C 1336 1216 1332 1214 1329 1218 C 1325 1223 1327 1230 1325 1235 C 1325 1235 1322 1246 1322 1246 C 1322 1250 1325 1253 1327 1256 C 1327 1256 1337 1273 1337 1273 C 1338 1275 1340 1279 1343 1280 C 1347 1281 1350 1277 1355 1275 C 1366 1273 1370 1281 1375 1289 C 1375 1289 1400 1284 1400 1284 C 1406 1283 1414 1283 1419 1280 C 1419 1280 1442 1257 1442 1257 C 1453 1248 1463 1244 1477 1241 C 1477 1241 1501 1237 1501 1237 C 1506 1236 1507 1233 1511 1233 C 1519 1232 1522 1247 1529 1244 C 1530 1243 1532 1242 1533 1241 C 1534 1240 1536 1238 1537 1237 C 1537 1237 1552 1247 1552 1247 C 1552 1247 1553 1242 1553 1242 C 1561 1246 1570 1250 1579 1251 C 1582 1251 1584 1250 1587 1250 C 1603 1249 1607 1247 1622 1242 C 1640 1236 1637 1241 1655 1229 C 1670 1218 1687 1202 1673 1183 C 1670 1179 1665 1173 1660 1171 C 1652 1167 1640 1168 1630 1164 C 1620 1159 1611 1151 1601 1145 C 1592 1140 1583 1136 1573 1133 C 1573 1133 1556 1129 1556 1129 C 1556 1129 1543 1124 1543 1124 C 1538 1123 1533 1122 1530 1117 C 1528 1115 1527 1111 1530 1109 C 1534 1108 1539 1112 1543 1110 C 1547 1108 1546 1102 1547 1098 C 1549 1088 1555 1079 1556 1069 C 1554 1069 1551 1069 1549 1069 C 1544 1068 1540 1063 1544 1058 C 1544 1058 1565 1042 1565 1042 C 1568 1040 1571 1037 1570 1032 z"/> + </g> + +</svg> diff --git a/diplomacy/maps/svg/svg.dtd b/diplomacy/maps/svg/svg.dtd new file mode 100644 index 0000000..49742cb --- /dev/null +++ b/diplomacy/maps/svg/svg.dtd @@ -0,0 +1,1846 @@ +<!-- ============================================================== + ENTITY DECLARATIONS: Data types + ============================================================== --> + +<!ENTITY % BaselineShiftValue "CDATA"> + <!-- 'baseline-shift' property/attribute value (e.g., 'baseline', 'sub', etc.) --> + +<!ENTITY % Boolean "(false | true)"> + <!-- feature specification --> + +<!ENTITY % ClassList "CDATA"> + <!-- list of classes --> + +<!ENTITY % ClipValue "CDATA"> + <!-- 'clip' property/attribute value (e.g., 'auto', rect(...)) --> + +<!ENTITY % ClipPathValue "CDATA"> + <!-- 'clip-path' property/attribute value (e.g., 'none', %URI;) --> + +<!ENTITY % ClipFillRule "(nonzero | evenodd | inherit)"> + <!-- 'clip-rule' or fill-rule property/attribute value --> + +<!ENTITY % ContentType "CDATA"> + <!-- media type, as per [RFC2045] --> + +<!ENTITY % Coordinate "CDATA"> + <!-- a <coordinate> --> + +<!ENTITY % Coordinates "CDATA"> + <!-- a list of <coordinate>s --> + +<!ENTITY % Color "CDATA"> + <!-- a <color> value --> + +<!ENTITY % CursorValue "CDATA"> + <!-- 'cursor' property/attribute value (e.g., 'crosshair', %URI;) --> + +<!ENTITY % EnableBackgroundValue "CDATA"> + <!-- 'enable-background' property/attribute value (e.g., 'new', 'accumulate') --> + +<!ENTITY % ExtensionList "CDATA"> + <!-- extension list specification --> + +<!ENTITY % FeatureList "CDATA"> + <!-- feature list specification --> + +<!ENTITY % FilterValue "CDATA"> + <!-- 'filter' property/attribute value (e.g., 'none', %URI;) --> + +<!ENTITY % FontFamilyValue "CDATA"> + <!-- 'font-family' property/attribute value (i.e., list of fonts) --> + +<!ENTITY % FontSizeValue "CDATA"> + <!-- 'font-size' property/attribute value --> + +<!ENTITY % FontSizeAdjustValue "CDATA"> + <!-- 'font-size-adjust' property/attribute value --> + +<!ENTITY % GlyphOrientationHorizontalValue "CDATA"> + <!-- 'glyph-orientation-horizontal' property/attribute value (e.g., <angle>) --> + +<!ENTITY % GlyphOrientationVerticalValue "CDATA"> + <!-- 'glyph-orientation-vertical' property/attribute value (e.g., 'auto', <angle>) --> + +<!ENTITY % Integer "CDATA"> + <!-- a <integer> --> + +<!ENTITY % KerningValue "CDATA"> + <!-- 'kerning' property/attribute value (e.g., auto | <length>) --> + +<!ENTITY % LanguageCode "NMTOKEN"> + <!-- a language code, as per [RFC3066] --> + +<!ENTITY % LanguageCodes "CDATA"> + <!-- comma-separated list of language codes, as per [RFC3066] --> + +<!ENTITY % Length "CDATA"> + <!-- a <length> --> + +<!ENTITY % Lengths "CDATA"> + <!-- a list of <length>s --> + +<!ENTITY % LinkTarget "NMTOKEN"> + <!-- link to this target --> + +<!ENTITY % MarkerValue "CDATA"> + <!-- 'marker' property/attribute value (e.g., 'none', %URI;) --> + +<!ENTITY % MaskValue "CDATA"> + <!-- 'mask' property/attribute value (e.g., 'none', %URI;) --> + +<!ENTITY % MediaDesc "CDATA"> + <!-- comma-separated list of media descriptors. --> + +<!ENTITY % Number "CDATA"> + <!-- a <number> --> + +<!ENTITY % NumberOptionalNumber "CDATA"> + <!-- list of <number>s, but at least one and at most two --> + +<!ENTITY % NumberOrPercentage "CDATA"> + <!-- a <number> or a <percentage> --> + +<!ENTITY % Numbers "CDATA"> + <!-- a list of <number>s --> + +<!ENTITY % OpacityValue "CDATA"> + <!-- opacity value (e.g., <number>) --> + +<!ENTITY % Paint "CDATA"> + <!-- a 'fill' or 'stroke' property/attribute value: <paint> --> + +<!ENTITY % PathData "CDATA"> + <!-- a path data specification --> + +<!ENTITY % Points "CDATA"> + <!-- a list of points --> + +<!ENTITY % PreserveAspectRatioSpec "CDATA"> + <!-- 'preserveAspectRatio' attribute specification --> + +<!ENTITY % Script "CDATA"> + <!-- script expression --> + +<!ENTITY % SpacingValue "CDATA"> + <!-- 'letter-spacing' or 'word-spacing' property/attribute value (e.g., normal | <length>) --> + +<!ENTITY % StrokeDashArrayValue "CDATA"> + <!-- 'stroke-dasharray' property/attribute value (e.g., 'none', list of <number>s) --> + +<!ENTITY % StrokeDashOffsetValue "CDATA"> + <!-- 'stroke-dashoffset' property/attribute value (e.g., 'none', <legnth>) --> + +<!ENTITY % StrokeMiterLimitValue "CDATA"> + <!-- 'stroke-miterlimit' property/attribute value (e.g., <number>) --> + +<!ENTITY % StrokeWidthValue "CDATA"> + <!-- 'stroke-width' property/attribute value (e.g., <length>) --> + +<!ENTITY % StructuredText + "content CDATA #FIXED 'structured text'" > + +<!ENTITY % StyleSheet "CDATA"> + <!-- style sheet data --> + +<!ENTITY % SVGColor "CDATA"> + <!-- An SVG color value (RGB plus optional ICC) --> + +<!ENTITY % Text "CDATA"> + <!-- arbitrary text string --> + +<!ENTITY % TextDecorationValue "CDATA"> + <!-- 'text-decoration' property/attribute value (e.g., 'none', 'underline') --> + +<!ENTITY % TransformList "CDATA"> + <!-- list of transforms --> + +<!ENTITY % URI "CDATA"> + <!-- a Uniform Resource Identifier, see [URI] --> + +<!ENTITY % ViewBoxSpec "CDATA"> + <!-- 'viewBox' attribute specification --> + + +<!-- ============================================================== + ENTITY DECLARATIONS: Collections of common attributes + ============================================================== --> + +<!-- All elements have an ID. --> +<!ENTITY % stdAttrs + "id ID #IMPLIED + xml:base %URI; #IMPLIED" > + +<!-- Common attributes for elements that might contain character data content. --> +<!ENTITY % langSpaceAttrs + "xml:lang %LanguageCode; #IMPLIED + xml:space (default|preserve) #IMPLIED" > + +<!-- Common attributes to check for system capabilities. --> +<!ENTITY % testAttrs + "requiredFeatures %FeatureList; #IMPLIED + requiredExtensions %ExtensionList; #IMPLIED + systemLanguage %LanguageCodes; #IMPLIED" > + +<!-- For most uses of URI referencing: + standard XLink attributes other than xlink:href. --> +<!ENTITY % xlinkRefAttrs + "xmlns:xlink CDATA #FIXED 'http://www.w3.org/1999/xlink' + xlink:type (simple) #FIXED 'simple' + xlink:role %URI; #IMPLIED + xlink:arcrole %URI; #IMPLIED + xlink:title CDATA #IMPLIED + xlink:show (other) 'other' + xlink:actuate (onLoad) #FIXED 'onLoad'" > + +<!-- Standard XLink attributes for uses of URI referencing where xlink:show is 'embed' --> +<!ENTITY % xlinkRefAttrsEmbed + "xmlns:xlink CDATA #FIXED 'http://www.w3.org/1999/xlink' + xlink:type (simple) #FIXED 'simple' + xlink:role %URI; #IMPLIED + xlink:arcrole %URI; #IMPLIED + xlink:title CDATA #IMPLIED + xlink:show (embed) 'embed' + xlink:actuate (onLoad) #FIXED 'onLoad'" > + +<!ENTITY % graphicsElementEvents + "onfocusin %Script; #IMPLIED + onfocusout %Script; #IMPLIED + onactivate %Script; #IMPLIED + onclick %Script; #IMPLIED + onmousedown %Script; #IMPLIED + onmouseup %Script; #IMPLIED + onmouseover %Script; #IMPLIED + onmousemove %Script; #IMPLIED + onmouseout %Script; #IMPLIED + onload %Script; #IMPLIED" > + +<!ENTITY % documentEvents + "onunload %Script; #IMPLIED + onabort %Script; #IMPLIED + onerror %Script; #IMPLIED + onresize %Script; #IMPLIED + onscroll %Script; #IMPLIED + onzoom %Script; #IMPLIED" > + +<!ENTITY % animationEvents + "onbegin %Script; #IMPLIED + onend %Script; #IMPLIED + onrepeat %Script; #IMPLIED" > + +<!-- This entity allows for at most one of desc, title and metadata, + supplied in any order --> +<!ENTITY % descTitleMetadata + "(((desc,((title,metadata?)|(metadata,title?))?)| + (title,((desc,metadata?)|(metadata,desc?))?)| + (metadata,((desc,title?)|(title,desc?))?))?)" > + + +<!-- ============================================================== + ENTITY DECLARATIONS: Collections of presentation attributes + ============================================================== --> + +<!-- The following presentation attributes have to do with specifying color. --> +<!ENTITY % PresentationAttributes-Color + "color %Color; #IMPLIED + color-interpolation (auto | sRGB | linearRGB | inherit) #IMPLIED + color-rendering (auto | optimizeSpeed | optimizeQuality | inherit) #IMPLIED " > + +<!-- The following presentation attributes apply to container elements. --> +<!ENTITY % PresentationAttributes-Containers + "enable-background %EnableBackgroundValue; #IMPLIED " > + +<!-- The following presentation attributes apply to 'feFlood' elements. --> +<!ENTITY % PresentationAttributes-feFlood + "flood-color %SVGColor; #IMPLIED + flood-opacity %OpacityValue; #IMPLIED " > + +<!-- The following presentation attributes apply to filling and stroking operations. --> +<!ENTITY % PresentationAttributes-FillStroke + "fill %Paint; #IMPLIED + fill-opacity %OpacityValue; #IMPLIED + fill-rule %ClipFillRule; #IMPLIED + stroke %Paint; #IMPLIED + stroke-dasharray %StrokeDashArrayValue; #IMPLIED + stroke-dashoffset %StrokeDashOffsetValue; #IMPLIED + stroke-linecap (butt | round | square | inherit) #IMPLIED + stroke-linejoin (miter | round | bevel | inherit) #IMPLIED + stroke-miterlimit %StrokeMiterLimitValue; #IMPLIED + stroke-opacity %OpacityValue; #IMPLIED + stroke-width %StrokeWidthValue; #IMPLIED " > + +<!-- The following presentation attributes apply to filter primitives. --> +<!ENTITY % PresentationAttributes-FilterPrimitives + "color-interpolation-filters (auto | sRGB | linearRGB | inherit) #IMPLIED " > + +<!-- The following presentation attributes have to do with selecting a font to use. --> +<!ENTITY % PresentationAttributes-FontSpecification + "font-family %FontFamilyValue; #IMPLIED + font-size %FontSizeValue; #IMPLIED + font-size-adjust %FontSizeAdjustValue; #IMPLIED + font-stretch (normal | wider | narrower | ultra-condensed | extra-condensed | + condensed | semi-condensed | semi-expanded | expanded | + extra-expanded | ultra-expanded | inherit) #IMPLIED + font-style (normal | italic | oblique | inherit) #IMPLIED + font-variant (normal | small-caps | inherit) #IMPLIED + font-weight (normal | bold | bolder | lighter | 100 | 200 | 300 | + 400 | 500 | 600 | 700 | 800 | 900 | inherit) #IMPLIED " > + +<!-- The following presentation attributes apply to gradient 'stop' elements. --> +<!ENTITY % PresentationAttributes-Gradients + "stop-color %SVGColor; #IMPLIED + stop-opacity %OpacityValue; #IMPLIED " > + +<!-- The following presentation attributes apply to graphics elements. --> +<!ENTITY % PresentationAttributes-Graphics + "clip-path %ClipPathValue; #IMPLIED + clip-rule %ClipFillRule; #IMPLIED + cursor %CursorValue; #IMPLIED + display (inline | block | list-item | run-in | compact | marker | + table | inline-table | table-row-group | table-header-group | + table-footer-group | table-row | table-column-group | table-column | + table-cell | table-caption | none | inherit) #IMPLIED + filter %FilterValue; #IMPLIED + image-rendering (auto | optimizeSpeed | optimizeQuality | inherit) #IMPLIED + mask %MaskValue; #IMPLIED + opacity %OpacityValue; #IMPLIED + pointer-events (visiblePainted | visibleFill | visibleStroke | visible | + painted | fill | stroke | all | none | inherit) #IMPLIED + shape-rendering (auto | optimizeSpeed | crispEdges | geometricPrecision | inherit) #IMPLIED + text-rendering (auto | optimizeSpeed | optimizeLegibility | geometricPrecision | inherit) #IMPLIED + visibility (visible | hidden | inherit) #IMPLIED " > + +<!-- The following presentation attributes apply to 'image' elements. --> +<!ENTITY % PresentationAttributes-Images + "color-profile CDATA #IMPLIED " > + +<!--The following presentation attributes apply to 'feDiffuseLighting' and 'feSpecularLighting' elements. --> +<!ENTITY % PresentationAttributes-LightingEffects + "lighting-color %SVGColor; #IMPLIED " > + +<!-- The following presentation attributes apply to marker operations. --> +<!ENTITY % PresentationAttributes-Markers + "marker-start %MarkerValue; #IMPLIED + marker-mid %MarkerValue; #IMPLIED + marker-end %MarkerValue; #IMPLIED " > + +<!-- The following presentation attributes apply to text content elements. --> +<!ENTITY % PresentationAttributes-TextContentElements + "alignment-baseline (baseline | top | before-edge | text-top | text-before-edge | + middle | bottom | after-edge | text-bottom | text-after-edge | + ideographic | lower | hanging | mathematical | inherit) #IMPLIED + baseline-shift %BaselineShiftValue; #IMPLIED + direction (ltr | rtl | inherit) #IMPLIED + dominant-baseline (auto | autosense-script | no-change | reset| + ideographic | lower | hanging | mathematical | inherit ) #IMPLIED + glyph-orientation-horizontal %GlyphOrientationHorizontalValue; #IMPLIED + glyph-orientation-vertical %GlyphOrientationVerticalValue; #IMPLIED + kerning %KerningValue; #IMPLIED + letter-spacing %SpacingValue; #IMPLIED + text-anchor (start | middle | end | inherit) #IMPLIED + text-decoration %TextDecorationValue; #IMPLIED + unicode-bidi (normal | embed | bidi-override | inherit) #IMPLIED + word-spacing %SpacingValue; #IMPLIED " > + +<!-- The following presentation attributes apply to 'text' elements. --> +<!ENTITY % PresentationAttributes-TextElements + "writing-mode (lr-tb | rl-tb | tb-rl | lr | rl | tb | inherit) #IMPLIED " > + +<!-- The following presentation attributes apply to elements that establish viewports. --> +<!ENTITY % PresentationAttributes-Viewports + "clip %ClipValue; #IMPLIED + overflow (visible | hidden | scroll | auto | inherit) #IMPLIED " > + +<!--The following represents the complete list of presentation attributes. --> +<!ENTITY % PresentationAttributes-All + "%PresentationAttributes-Color; + %PresentationAttributes-Containers; + %PresentationAttributes-feFlood; + %PresentationAttributes-FillStroke; + %PresentationAttributes-FilterPrimitives; + %PresentationAttributes-FontSpecification; + %PresentationAttributes-Gradients; + %PresentationAttributes-Graphics; + %PresentationAttributes-Images; + %PresentationAttributes-LightingEffects; + %PresentationAttributes-Markers; + %PresentationAttributes-TextContentElements; + %PresentationAttributes-TextElements; + %PresentationAttributes-Viewports;" > + + + +<!-- ============================================================== + ENTITY DECLARATIONS: DTD extensions + ============================================================== --> + +<!-- Allow for extending the DTD with internal subset for + container and graphics elements --> +<!ENTITY % ceExt "" > +<!ENTITY % geExt "" > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Document Structure + ============================================================== --> + +<!ENTITY % svgExt "" > +<!ELEMENT svg (desc|title|metadata|defs| + path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|view|switch|a|altGlyphDef| + script|style|symbol|marker|clipPath|mask| + linearGradient|radialGradient|pattern|filter|cursor|font| + animate|set|animateMotion|animateColor|animateTransform| + color-profile|font-face|jdipNS:ORDERDRAWING|jdipNS:DISPLAY|jdipNS:PROVINCE_DATA + %ceExt;%svgExt;)* > +<!ATTLIST svg + xmlns CDATA #FIXED "http://www.w3.org/2000/svg" + xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink" + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + viewBox %ViewBoxSpec; #IMPLIED + preserveAspectRatio %PreserveAspectRatioSpec; 'xMidYMid meet' + zoomAndPan (disable | magnify) 'magnify' + %graphicsElementEvents; + %documentEvents; + version %Number; #FIXED "1.0" + x %Coordinate; #IMPLIED + y %Coordinate; #IMPLIED + width %Length; #IMPLIED + height %Length; #IMPLIED + contentScriptType %ContentType; "text/ecmascript" + contentStyleType %ContentType; "text/css" > + +<!ENTITY % gExt "" > +<!ELEMENT g (desc|title|metadata|defs| + path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|view|switch|a|altGlyphDef| + script|style|symbol|marker|clipPath|mask| + linearGradient|radialGradient|pattern|filter|cursor|font| + animate|set|animateMotion|animateColor|animateTransform| + color-profile|font-face + %ceExt;%gExt;)* > +<!ATTLIST g + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + transform %TransformList; #IMPLIED + %graphicsElementEvents; > + +<!ENTITY % defsExt "" > +<!ELEMENT defs (desc|title|metadata|defs| + path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|view|switch|a|altGlyphDef| + script|style|symbol|marker|clipPath|mask| + linearGradient|radialGradient|pattern|filter|cursor|font| + animate|set|animateMotion|animateColor|animateTransform| + color-profile|font-face + %ceExt;%defsExt;)* > +<!ATTLIST defs + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + transform %TransformList; #IMPLIED + %graphicsElementEvents; > + +<!ENTITY % descExt "" > +<!ELEMENT desc (#PCDATA %descExt;)* > +<!ATTLIST desc + %stdAttrs; + %langSpaceAttrs; + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %StructuredText; > + +<!ENTITY % titleExt "" > +<!ELEMENT title (#PCDATA %titleExt;)* > +<!ATTLIST title + %stdAttrs; + %langSpaceAttrs; + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %StructuredText; > + +<!ENTITY % symbolExt "" > +<!ELEMENT symbol (desc|title|metadata|defs| + path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|view|switch|a|altGlyphDef| + script|style|symbol|marker|clipPath|mask| + linearGradient|radialGradient|pattern|filter|cursor|font| + animate|set|animateMotion|animateColor|animateTransform| + color-profile|font-face + %ceExt;%symbolExt;)* > +<!ATTLIST symbol + %stdAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + viewBox %ViewBoxSpec; #IMPLIED + preserveAspectRatio %PreserveAspectRatioSpec; 'xMidYMid meet' + %graphicsElementEvents; > + +<!ENTITY % useExt "" > +<!ELEMENT use (%descTitleMetadata;,(animate|set|animateMotion|animateColor|animateTransform + %geExt;%useExt;)*) > +<!ATTLIST use + %stdAttrs; + %xlinkRefAttrsEmbed; + xlink:href %URI; #REQUIRED + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + x %Coordinate; #IMPLIED + y %Coordinate; #IMPLIED + width %Length; #IMPLIED + height %Length; #IMPLIED > + +<!ENTITY % imageExt "" > +<!ELEMENT image (%descTitleMetadata;,(animate|set|animateMotion|animateColor|animateTransform + %geExt;%imageExt;)*) > +<!ATTLIST image + %stdAttrs; + %xlinkRefAttrsEmbed; + xlink:href %URI; #REQUIRED + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-Graphics; + %PresentationAttributes-Images; + %PresentationAttributes-Viewports; + transform %TransformList; #IMPLIED + preserveAspectRatio %PreserveAspectRatioSpec; 'xMidYMid meet' + %graphicsElementEvents; + x %Coordinate; #IMPLIED + y %Coordinate; #IMPLIED + width %Length; #REQUIRED + height %Length; #REQUIRED > + +<!ENTITY % switchExt "" > +<!ELEMENT switch (%descTitleMetadata;, + (path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|switch|a|foreignObject| + animate|set|animateMotion|animateColor|animateTransform + %ceExt;%switchExt;)*) > +<!ATTLIST switch + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + transform %TransformList; #IMPLIED + %graphicsElementEvents; > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Styling + ============================================================== --> + +<!ELEMENT style (#PCDATA) > +<!ATTLIST style + %stdAttrs; + xml:space (preserve) #FIXED "preserve" + type %ContentType; #REQUIRED + media %MediaDesc; #IMPLIED + title %Text; #IMPLIED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Paths + ============================================================== --> + +<!ENTITY % pathExt "" > +<!ELEMENT path (%descTitleMetadata;,(animate|set|animateMotion|animateColor|animateTransform + %geExt;%pathExt;)*) > +<!ATTLIST path + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-Graphics; + %PresentationAttributes-Markers; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + d %PathData; #REQUIRED + pathLength %Number; #IMPLIED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Basic Shapes + ============================================================== --> + +<!ENTITY % rectExt "" > +<!ELEMENT rect (%descTitleMetadata;,(animate|set|animateMotion|animateColor|animateTransform + %geExt;%rectExt;)*) > +<!ATTLIST rect + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-Graphics; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + x %Coordinate; #IMPLIED + y %Coordinate; #IMPLIED + width %Length; #REQUIRED + height %Length; #REQUIRED + rx %Length; #IMPLIED + ry %Length; #IMPLIED > + +<!ENTITY % circleExt "" > +<!ELEMENT circle (%descTitleMetadata;,(animate|set|animateMotion|animateColor|animateTransform + %geExt;%circleExt;)*) > +<!ATTLIST circle + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-Graphics; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + cx %Coordinate; #IMPLIED + cy %Coordinate; #IMPLIED + r %Length; #REQUIRED > + +<!ENTITY % ellipseExt "" > +<!ELEMENT ellipse (%descTitleMetadata;,(animate|set|animateMotion|animateColor|animateTransform + %geExt;%ellipseExt;)*) > +<!ATTLIST ellipse + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-Graphics; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + cx %Coordinate; #IMPLIED + cy %Coordinate; #IMPLIED + rx %Length; #REQUIRED + ry %Length; #REQUIRED > + +<!ENTITY % lineExt "" > +<!ELEMENT line (%descTitleMetadata;,(animate|set|animateMotion|animateColor|animateTransform + %geExt;%lineExt;)*) > +<!ATTLIST line + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-Graphics; + %PresentationAttributes-Markers; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + x1 %Coordinate; #IMPLIED + y1 %Coordinate; #IMPLIED + x2 %Coordinate; #IMPLIED + y2 %Coordinate; #IMPLIED > + +<!ENTITY % polylineExt "" > +<!ELEMENT polyline (%descTitleMetadata;,(animate|set|animateMotion|animateColor|animateTransform + %geExt;%polylineExt;)*) > +<!ATTLIST polyline + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-Graphics; + %PresentationAttributes-Markers; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + points %Points; #REQUIRED > + +<!ENTITY % polygonExt "" > +<!ELEMENT polygon (%descTitleMetadata;,(animate|set|animateMotion|animateColor|animateTransform + %geExt;%polygonExt;)*) > +<!ATTLIST polygon + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-Graphics; + %PresentationAttributes-Markers; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + points %Points; #REQUIRED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Text + ============================================================== --> + +<!ENTITY % textExt "" > +<!ELEMENT text (#PCDATA|desc|title|metadata| + tspan|tref|textPath|altGlyph|a|animate|set| + animateMotion|animateColor|animateTransform + %geExt;%textExt;)* > +<!ATTLIST text + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-FontSpecification; + %PresentationAttributes-Graphics; + %PresentationAttributes-TextContentElements; + %PresentationAttributes-TextElements; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + x %Coordinates; #IMPLIED + y %Coordinates; #IMPLIED + dx %Lengths; #IMPLIED + dy %Lengths; #IMPLIED + rotate %Numbers; #IMPLIED + textLength %Length; #IMPLIED + lengthAdjust (spacing|spacingAndGlyphs) #IMPLIED > + +<!ENTITY % tspanExt "" > +<!ELEMENT tspan (#PCDATA|desc|title|metadata|tspan|tref|altGlyph|a|animate|set|animateColor + %tspanExt;)* > +<!ATTLIST tspan + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-FontSpecification; + %PresentationAttributes-Graphics; + %PresentationAttributes-TextContentElements; + %graphicsElementEvents; + x %Coordinates; #IMPLIED + y %Coordinates; #IMPLIED + dx %Lengths; #IMPLIED + dy %Lengths; #IMPLIED + rotate %Numbers; #IMPLIED + textLength %Length; #IMPLIED + lengthAdjust (spacing|spacingAndGlyphs) #IMPLIED > + +<!ENTITY % trefExt "" > +<!ELEMENT tref (desc|title|metadata|animate|set|animateColor + %trefExt;)* > +<!ATTLIST tref + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #REQUIRED + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-FontSpecification; + %PresentationAttributes-Graphics; + %PresentationAttributes-TextContentElements; + %graphicsElementEvents; + x %Coordinates; #IMPLIED + y %Coordinates; #IMPLIED + dx %Lengths; #IMPLIED + dy %Lengths; #IMPLIED + rotate %Numbers; #IMPLIED + textLength %Length; #IMPLIED + lengthAdjust (spacing|spacingAndGlyphs) #IMPLIED > + +<!ENTITY % textPathExt "" > +<!ELEMENT textPath (#PCDATA|desc|title|metadata|tspan|tref|altGlyph|a|animate|set|animateColor + %textPathExt;)* > +<!ATTLIST textPath + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #REQUIRED + %langSpaceAttrs; + %testAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-FontSpecification; + %PresentationAttributes-Graphics; + %PresentationAttributes-TextContentElements; + %graphicsElementEvents; + startOffset %Length; #IMPLIED + textLength %Length; #IMPLIED + lengthAdjust (spacing|spacingAndGlyphs) #IMPLIED + method (align|stretch) #IMPLIED + spacing (auto|exact) #IMPLIED > + +<!ENTITY % altGlyphExt "" > +<!ELEMENT altGlyph (#PCDATA %altGlyphExt;)* > +<!ATTLIST altGlyph + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #IMPLIED + glyphRef CDATA #IMPLIED + format CDATA #IMPLIED + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-FontSpecification; + %PresentationAttributes-Graphics; + %PresentationAttributes-TextContentElements; + %graphicsElementEvents; + x %Coordinates; #IMPLIED + y %Coordinates; #IMPLIED + dx %Lengths; #IMPLIED + dy %Lengths; #IMPLIED + rotate %Numbers; #IMPLIED > + +<!ENTITY % altGlyphDefExt "" > +<!ELEMENT altGlyphDef ((glyphRef+|altGlyphItem+) %altGlyphDefExt;) > +<!ATTLIST altGlyphDef + %stdAttrs; > + +<!ENTITY % altGlyphItemExt "" > +<!ELEMENT altGlyphItem (glyphRef+ %altGlyphItemExt;) > +<!ATTLIST altGlyphItem + %stdAttrs; > + +<!ELEMENT glyphRef EMPTY > +<!ATTLIST glyphRef + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-FontSpecification; + glyphRef CDATA #IMPLIED + format CDATA #IMPLIED + x %Number; #IMPLIED + y %Number; #IMPLIED + dx %Number; #IMPLIED + dy %Number; #IMPLIED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Painting: Filling, Stroking and Marker Symbols + ============================================================== --> + +<!ENTITY % markerExt "" > +<!ELEMENT marker (desc|title|metadata|defs| + path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|view|switch|a|altGlyphDef| + script|style|symbol|marker|clipPath|mask| + linearGradient|radialGradient|pattern|filter|cursor|font| + animate|set|animateMotion|animateColor|animateTransform| + color-profile|font-face + %ceExt;%markerExt;)* > +<!ATTLIST marker + %stdAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + viewBox %ViewBoxSpec; #IMPLIED + preserveAspectRatio %PreserveAspectRatioSpec; 'xMidYMid meet' + refX %Coordinate; #IMPLIED + refY %Coordinate; #IMPLIED + markerUnits (strokeWidth | userSpaceOnUse) #IMPLIED + markerWidth %Length; #IMPLIED + markerHeight %Length; #IMPLIED + orient CDATA #IMPLIED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Color + ============================================================== --> + +<!ELEMENT color-profile (%descTitleMetadata;) > +<!ATTLIST color-profile + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #IMPLIED + local CDATA #IMPLIED + name CDATA #REQUIRED + rendering-intent (auto | perceptual | relative-colorimetric | saturation | absolute-colorimetric) "auto" > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Gradients and Patterns + ============================================================== --> + +<!ENTITY % linearGradientExt "" > +<!ELEMENT linearGradient (%descTitleMetadata;,(stop|animate|set|animateTransform + %linearGradientExt;)*) > +<!ATTLIST linearGradient + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #IMPLIED + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-Gradients; + gradientUnits (userSpaceOnUse | objectBoundingBox) #IMPLIED + gradientTransform %TransformList; #IMPLIED + x1 %Coordinate; #IMPLIED + y1 %Coordinate; #IMPLIED + x2 %Coordinate; #IMPLIED + y2 %Coordinate; #IMPLIED + spreadMethod (pad | reflect | repeat) #IMPLIED > + + +<!ENTITY % radialGradientExt "" > +<!ELEMENT radialGradient (%descTitleMetadata;,(stop|animate|set|animateTransform + %radialGradientExt;)*) > +<!ATTLIST radialGradient + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #IMPLIED + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-Gradients; + gradientUnits (userSpaceOnUse | objectBoundingBox) #IMPLIED + gradientTransform %TransformList; #IMPLIED + cx %Coordinate; #IMPLIED + cy %Coordinate; #IMPLIED + r %Length; #IMPLIED + fx %Coordinate; #IMPLIED + fy %Coordinate; #IMPLIED + spreadMethod (pad | reflect | repeat) #IMPLIED > + + +<!ENTITY % stopExt "" > +<!ELEMENT stop (animate|set|animateColor + %stopExt;)* > +<!ATTLIST stop + %stdAttrs; + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-Gradients; + offset %NumberOrPercentage; #REQUIRED > + +<!ENTITY % patternExt "" > +<!ELEMENT pattern (desc|title|metadata|defs| + path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|view|switch|a|altGlyphDef| + script|style|symbol|marker|clipPath|mask| + linearGradient|radialGradient|pattern|filter|cursor|font| + animate|set|animateMotion|animateColor|animateTransform| + color-profile|font-face + %ceExt;%patternExt;)* > +<!ATTLIST pattern + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #IMPLIED + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + viewBox %ViewBoxSpec; #IMPLIED + preserveAspectRatio %PreserveAspectRatioSpec; 'xMidYMid meet' + patternUnits (userSpaceOnUse | objectBoundingBox) #IMPLIED + patternContentUnits (userSpaceOnUse | objectBoundingBox) #IMPLIED + patternTransform %TransformList; #IMPLIED + x %Coordinate; #IMPLIED + y %Coordinate; #IMPLIED + width %Length; #IMPLIED + height %Length; #IMPLIED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Clipping, Masking and Compositing + ============================================================== --> + +<!ENTITY % clipPathExt "" > +<!ELEMENT clipPath (%descTitleMetadata;, + (path|text|rect|circle|ellipse|line|polyline|polygon| + use|animate|set|animateMotion|animateColor|animateTransform + %ceExt;%clipPathExt;)*) > +<!ATTLIST clipPath + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FillStroke; + %PresentationAttributes-FontSpecification; + %PresentationAttributes-Graphics; + %PresentationAttributes-TextContentElements; + %PresentationAttributes-TextElements; + transform %TransformList; #IMPLIED + clipPathUnits (userSpaceOnUse | objectBoundingBox) #IMPLIED > + +<!ENTITY % maskExt "" > +<!ELEMENT mask (desc|title|metadata|defs| + path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|view|switch|a|altGlyphDef| + script|style|symbol|marker|clipPath|mask| + linearGradient|radialGradient|pattern|filter|cursor|font| + animate|set|animateMotion|animateColor|animateTransform| + color-profile|font-face + %ceExt;%maskExt;)* > +<!ATTLIST mask + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + maskUnits (userSpaceOnUse | objectBoundingBox) #IMPLIED + maskContentUnits (userSpaceOnUse | objectBoundingBox) #IMPLIED + x %Coordinate; #IMPLIED + y %Coordinate; #IMPLIED + width %Length; #IMPLIED + height %Length; #IMPLIED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Filter Effects + ============================================================== --> + +<!ENTITY % filterExt "" > +<!ELEMENT filter (%descTitleMetadata;,(feBlend|feFlood| + feColorMatrix|feComponentTransfer| + feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap| + feGaussianBlur|feImage|feMerge| + feMorphology|feOffset|feSpecularLighting| + feTile|feTurbulence| + animate|set + %filterExt;)*) > +<!ATTLIST filter + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #IMPLIED + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + filterUnits (userSpaceOnUse | objectBoundingBox) #IMPLIED + primitiveUnits (userSpaceOnUse | objectBoundingBox) #IMPLIED + x %Coordinate; #IMPLIED + y %Coordinate; #IMPLIED + width %Length; #IMPLIED + height %Length; #IMPLIED + filterRes %NumberOptionalNumber; #IMPLIED > + +<!ENTITY % filter_primitive_attributes + "x %Coordinate; #IMPLIED + y %Coordinate; #IMPLIED + width %Length; #IMPLIED + height %Length; #IMPLIED + result CDATA #IMPLIED" > + +<!ENTITY % filter_primitive_attributes_with_in + "%filter_primitive_attributes; + in CDATA #IMPLIED"> + +<!ELEMENT feDistantLight (animate|set)* > +<!ATTLIST feDistantLight + %stdAttrs; + azimuth %Number; #IMPLIED + elevation %Number; #IMPLIED > + +<!ELEMENT fePointLight (animate|set)* > +<!ATTLIST fePointLight + %stdAttrs; + x %Number; #IMPLIED + y %Number; #IMPLIED + z %Number; #IMPLIED > + +<!ELEMENT feSpotLight (animate|set)* > +<!ATTLIST feSpotLight + %stdAttrs; + x %Number; #IMPLIED + y %Number; #IMPLIED + z %Number; #IMPLIED + pointsAtX %Number; #IMPLIED + pointsAtY %Number; #IMPLIED + pointsAtZ %Number; #IMPLIED + specularExponent %Number; #IMPLIED + limitingConeAngle %Number; #IMPLIED > + +<!ELEMENT feBlend (animate|set)* > +<!ATTLIST feBlend + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; + in2 CDATA #REQUIRED + mode (normal | multiply | screen | darken | lighten) "normal" > + +<!ELEMENT feColorMatrix (animate|set)* > +<!ATTLIST feColorMatrix + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; + type (matrix | saturate | hueRotate | luminanceToAlpha) "matrix" + values CDATA #IMPLIED > + +<!ELEMENT feComponentTransfer (feFuncR?,feFuncG?,feFuncB?,feFuncA?) > +<!ATTLIST feComponentTransfer + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; > + +<!ENTITY % component_transfer_function_attributes + "type (identity | table | discrete | linear | gamma) #REQUIRED + tableValues CDATA #IMPLIED + slope %Number; #IMPLIED + intercept %Number; #IMPLIED + amplitude %Number; #IMPLIED + exponent %Number; #IMPLIED + offset %Number; #IMPLIED" > + +<!ELEMENT feFuncR (animate|set)* > +<!ATTLIST feFuncR + %stdAttrs; + %component_transfer_function_attributes; > + +<!ELEMENT feFuncG (animate|set)* > +<!ATTLIST feFuncG + %stdAttrs; + %component_transfer_function_attributes; > + +<!ELEMENT feFuncB (animate|set)* > +<!ATTLIST feFuncB + %stdAttrs; + %component_transfer_function_attributes; > + +<!ELEMENT feFuncA (animate|set)* > +<!ATTLIST feFuncA + %stdAttrs; + %component_transfer_function_attributes; > + +<!ELEMENT feComposite (animate|set)* > +<!ATTLIST feComposite + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; + in2 CDATA #REQUIRED + operator (over | in | out | atop | xor | arithmetic) "over" + k1 %Number; #IMPLIED + k2 %Number; #IMPLIED + k3 %Number; #IMPLIED + k4 %Number; #IMPLIED > + +<!ELEMENT feConvolveMatrix (animate|set)* > +<!ATTLIST feConvolveMatrix + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; + order %NumberOptionalNumber; #REQUIRED + kernelMatrix CDATA #REQUIRED + divisor %Number; #IMPLIED + bias %Number; #IMPLIED + targetX %Integer; #IMPLIED + targetY %Integer; #IMPLIED + edgeMode (duplicate|wrap|none) "duplicate" + kernelUnitLength %NumberOptionalNumber; #IMPLIED + preserveAlpha %Boolean; #IMPLIED > + +<!ELEMENT feDiffuseLighting ((feDistantLight|fePointLight|feSpotLight),(animate|set|animateColor)*) > +<!ATTLIST feDiffuseLighting + %stdAttrs; + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FilterPrimitives; + %PresentationAttributes-LightingEffects; + %filter_primitive_attributes_with_in; + surfaceScale %Number; #IMPLIED + diffuseConstant %Number; #IMPLIED + kernelUnitLength %NumberOptionalNumber; #IMPLIED > + +<!ELEMENT feDisplacementMap (animate|set)* > +<!ATTLIST feDisplacementMap + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; + in2 CDATA #REQUIRED + scale %Number; #IMPLIED + xChannelSelector (R | G | B | A) "A" + yChannelSelector (R | G | B | A) "A" > + +<!ELEMENT feFlood (animate|set|animateColor)* > +<!ATTLIST feFlood + %stdAttrs; + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-feFlood; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; > + +<!ELEMENT feGaussianBlur (animate|set)* > +<!ATTLIST feGaussianBlur + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; + stdDeviation %NumberOptionalNumber; #IMPLIED > + +<!ELEMENT feImage (animate|set|animateTransform)* > +<!ATTLIST feImage + %stdAttrs; + %xlinkRefAttrsEmbed; + xlink:href %URI; #REQUIRED + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + %filter_primitive_attributes; + preserveAspectRatio %PreserveAspectRatioSpec; 'xMidYMid meet' > + +<!ELEMENT feMerge (feMergeNode)* > +<!ATTLIST feMerge + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes; > + +<!ELEMENT feMergeNode (animate|set)* > +<!ATTLIST feMergeNode + %stdAttrs; + in CDATA #IMPLIED > + +<!ELEMENT feMorphology (animate|set)* > +<!ATTLIST feMorphology + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; + operator (erode | dilate) "erode" + radius %NumberOptionalNumber; #IMPLIED > + +<!ELEMENT feOffset (animate|set)* > +<!ATTLIST feOffset + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; + dx %Number; #IMPLIED + dy %Number; #IMPLIED > + +<!ELEMENT feSpecularLighting ((feDistantLight|fePointLight|feSpotLight),(animate|set|animateColor)*) > +<!ATTLIST feSpecularLighting + %stdAttrs; + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-Color; + %PresentationAttributes-FilterPrimitives; + %PresentationAttributes-LightingEffects; + %filter_primitive_attributes_with_in; + surfaceScale %Number; #IMPLIED + specularConstant %Number; #IMPLIED + specularExponent %Number; #IMPLIED + kernelUnitLength %NumberOptionalNumber; #IMPLIED > + +<!ELEMENT feTile (animate|set)* > +<!ATTLIST feTile + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes_with_in; > + +<!ELEMENT feTurbulence (animate|set)* > +<!ATTLIST feTurbulence + %stdAttrs; + %PresentationAttributes-FilterPrimitives; + %filter_primitive_attributes; + baseFrequency %NumberOptionalNumber; #IMPLIED + numOctaves %Integer; #IMPLIED + seed %Number; #IMPLIED + stitchTiles (stitch | noStitch) "noStitch" + type (fractalNoise | turbulence) "turbulence" > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Interactivity + ============================================================== --> + +<!ELEMENT cursor (%descTitleMetadata;) > +<!ATTLIST cursor + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #REQUIRED + %testAttrs; + externalResourcesRequired %Boolean; #IMPLIED + x %Coordinate; #IMPLIED + y %Coordinate; #IMPLIED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Linking + ============================================================== --> + +<!ENTITY % aExt "" > +<!ELEMENT a (#PCDATA|desc|title|metadata|defs| + path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|view|switch|a|altGlyphDef| + script|style|symbol|marker|clipPath|mask| + linearGradient|radialGradient|pattern|filter|cursor|font| + animate|set|animateMotion|animateColor|animateTransform| + color-profile|font-face + %ceExt;%aExt;)* > +<!ATTLIST a + %stdAttrs; + xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink" + xlink:type (simple) #FIXED "simple" + xlink:role %URI; #IMPLIED + xlink:arcrole %URI; #IMPLIED + xlink:title CDATA #IMPLIED + xlink:show (new|replace) 'replace' + xlink:actuate (onRequest) #FIXED 'onRequest' + xlink:href %URI; #REQUIRED + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + target %LinkTarget; #IMPLIED > + +<!ENTITY % viewExt "" > +<!ELEMENT view (%descTitleMetadata;%viewExt;) > +<!ATTLIST view + %stdAttrs; + externalResourcesRequired %Boolean; #IMPLIED + viewBox %ViewBoxSpec; #IMPLIED + preserveAspectRatio %PreserveAspectRatioSpec; 'xMidYMid meet' + zoomAndPan (disable | magnify) 'magnify' + viewTarget CDATA #IMPLIED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Scripting + ============================================================== --> + +<!ELEMENT script (#PCDATA) > +<!ATTLIST script + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #IMPLIED + externalResourcesRequired %Boolean; #IMPLIED + type %ContentType; #REQUIRED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Animation + ============================================================== --> + +<!ENTITY % animElementAttrs + "%xlinkRefAttrs; + xlink:href %URI; #IMPLIED" > + +<!ENTITY % animAttributeAttrs + "attributeName CDATA #REQUIRED + attributeType CDATA #IMPLIED" > + +<!ENTITY % animTimingAttrs + "begin CDATA #IMPLIED + dur CDATA #IMPLIED + end CDATA #IMPLIED + min CDATA #IMPLIED + max CDATA #IMPLIED + restart (always | never | whenNotActive) 'always' + repeatCount CDATA #IMPLIED + repeatDur CDATA #IMPLIED + fill (remove | freeze) 'remove'" > + +<!ENTITY % animValueAttrs + "calcMode (discrete | linear | paced | spline) 'linear' + values CDATA #IMPLIED + keyTimes CDATA #IMPLIED + keySplines CDATA #IMPLIED + from CDATA #IMPLIED + to CDATA #IMPLIED + by CDATA #IMPLIED" > + +<!ENTITY % animAdditionAttrs + "additive (replace | sum) 'replace' + accumulate (none | sum) 'none'" > + +<!ENTITY % animateExt "" > +<!ELEMENT animate (%descTitleMetadata;%animateExt;) > +<!ATTLIST animate + %stdAttrs; + %testAttrs; + externalResourcesRequired %Boolean; #IMPLIED + %animationEvents; + %animElementAttrs; + %animAttributeAttrs; + %animTimingAttrs; + %animValueAttrs; + %animAdditionAttrs; > + +<!ENTITY % setExt "" > +<!ELEMENT set (%descTitleMetadata;%setExt;) > +<!ATTLIST set + %stdAttrs; + %testAttrs; + externalResourcesRequired %Boolean; #IMPLIED + %animationEvents; + %animElementAttrs; + %animAttributeAttrs; + %animTimingAttrs; + to CDATA #IMPLIED > + +<!ENTITY % animateMotionExt "" > +<!ELEMENT animateMotion (%descTitleMetadata;,mpath? %animateMotionExt;) > +<!ATTLIST animateMotion + %stdAttrs; + %testAttrs; + externalResourcesRequired %Boolean; #IMPLIED + %animationEvents; + %animElementAttrs; + %animTimingAttrs; + calcMode (discrete | linear | paced | spline) 'paced' + values CDATA #IMPLIED + keyTimes CDATA #IMPLIED + keySplines CDATA #IMPLIED + from CDATA #IMPLIED + to CDATA #IMPLIED + by CDATA #IMPLIED + %animAdditionAttrs; + path CDATA #IMPLIED + keyPoints CDATA #IMPLIED + rotate CDATA #IMPLIED + origin CDATA #IMPLIED > + +<!ENTITY % mpathExt "" > +<!ELEMENT mpath (%descTitleMetadata;%mpathExt;) > +<!ATTLIST mpath + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #REQUIRED + externalResourcesRequired %Boolean; #IMPLIED > + +<!ENTITY % animateColorExt "" > +<!ELEMENT animateColor (%descTitleMetadata;%animateColorExt;) > +<!ATTLIST animateColor + %stdAttrs; + %testAttrs; + externalResourcesRequired %Boolean; #IMPLIED + %animationEvents; + %animElementAttrs; + %animAttributeAttrs; + %animTimingAttrs; + %animValueAttrs; + %animAdditionAttrs; > + +<!ENTITY % animateTransformExt "" > +<!ELEMENT animateTransform (%descTitleMetadata;%animateTransformExt;) > +<!ATTLIST animateTransform + %stdAttrs; + %testAttrs; + externalResourcesRequired %Boolean; #IMPLIED + %animationEvents; + %animElementAttrs; + %animAttributeAttrs; + %animTimingAttrs; + %animValueAttrs; + %animAdditionAttrs; + type (translate | scale | rotate | skewX | skewY) "translate" > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Fonts + ============================================================== --> + +<!ENTITY % fontExt "" > +<!ELEMENT font (%descTitleMetadata;,font-face, + missing-glyph,(glyph|hkern|vkern %fontExt;)*) > +<!ATTLIST font + %stdAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + horiz-origin-x %Number; #IMPLIED + horiz-origin-y %Number; #IMPLIED + horiz-adv-x %Number; #REQUIRED + vert-origin-x %Number; #IMPLIED + vert-origin-y %Number; #IMPLIED + vert-adv-y %Number; #IMPLIED > + +<!ENTITY % glyphExt "" > +<!ELEMENT glyph (desc|title|metadata|defs| + path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|view|switch|a|altGlyphDef| + script|style|symbol|marker|clipPath|mask| + linearGradient|radialGradient|pattern|filter|cursor|font| + animate|set|animateMotion|animateColor|animateTransform| + color-profile|font-face + %glyphExt;)* > +<!ATTLIST glyph + %stdAttrs; + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + unicode CDATA #IMPLIED + glyph-name CDATA #IMPLIED + d %PathData; #IMPLIED + orientation CDATA #IMPLIED + arabic-form CDATA #IMPLIED + lang %LanguageCodes; #IMPLIED + horiz-adv-x %Number; #IMPLIED + vert-origin-x %Number; #IMPLIED + vert-origin-y %Number; #IMPLIED + vert-adv-y %Number; #IMPLIED > + +<!ENTITY % missing-glyphExt "" > +<!ELEMENT missing-glyph (desc|title|metadata|defs| + path|text|rect|circle|ellipse|line|polyline|polygon| + use|image|svg|g|view|switch|a|altGlyphDef| + script|style|symbol|marker|clipPath|mask| + linearGradient|radialGradient|pattern|filter|cursor|font| + animate|set|animateMotion|animateColor|animateTransform| + color-profile|font-face + %missing-glyphExt;)* > +<!ATTLIST missing-glyph + %stdAttrs; + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + d %PathData; #IMPLIED + horiz-adv-x %Number; #IMPLIED + vert-origin-x %Number; #IMPLIED + vert-origin-y %Number; #IMPLIED + vert-adv-y %Number; #IMPLIED > + +<!ELEMENT hkern EMPTY > +<!ATTLIST hkern + %stdAttrs; + u1 CDATA #IMPLIED + g1 CDATA #IMPLIED + u2 CDATA #IMPLIED + g2 CDATA #IMPLIED + k %Number; #REQUIRED > + +<!ELEMENT vkern EMPTY > +<!ATTLIST vkern + %stdAttrs; + u1 CDATA #IMPLIED + g1 CDATA #IMPLIED + u2 CDATA #IMPLIED + g2 CDATA #IMPLIED + k %Number; #REQUIRED > + +<!ELEMENT font-face (%descTitleMetadata;,font-face-src?,definition-src?) > +<!ATTLIST font-face + %stdAttrs; + font-family CDATA #IMPLIED + font-style CDATA #IMPLIED + font-variant CDATA #IMPLIED + font-weight CDATA #IMPLIED + font-stretch CDATA #IMPLIED + font-size CDATA #IMPLIED + unicode-range CDATA #IMPLIED + units-per-em %Number; #IMPLIED + panose-1 CDATA #IMPLIED + stemv %Number; #IMPLIED + stemh %Number; #IMPLIED + slope %Number; #IMPLIED + cap-height %Number; #IMPLIED + x-height %Number; #IMPLIED + accent-height %Number; #IMPLIED + ascent %Number; #IMPLIED + descent %Number; #IMPLIED + widths CDATA #IMPLIED + bbox CDATA #IMPLIED + ideographic %Number; #IMPLIED + alphabetic %Number; #IMPLIED + mathematical %Number; #IMPLIED + hanging %Number; #IMPLIED + v-ideographic %Number; #IMPLIED + v-alphabetic %Number; #IMPLIED + v-mathematical %Number; #IMPLIED + v-hanging %Number; #IMPLIED + underline-position %Number; #IMPLIED + underline-thickness %Number; #IMPLIED + strikethrough-position %Number; #IMPLIED + strikethrough-thickness %Number; #IMPLIED + overline-position %Number; #IMPLIED + overline-thickness %Number; #IMPLIED > + +<!ELEMENT font-face-src (font-face-uri|font-face-name)+ > +<!ATTLIST font-face-src + %stdAttrs; > + +<!ELEMENT font-face-uri (font-face-format*) > +<!ATTLIST font-face-uri + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #REQUIRED > + +<!ELEMENT font-face-format EMPTY > +<!ATTLIST font-face-format + %stdAttrs; + string CDATA #IMPLIED > + +<!ELEMENT font-face-name EMPTY > +<!ATTLIST font-face-name + %stdAttrs; + name CDATA #IMPLIED > + +<!ELEMENT definition-src EMPTY > +<!ATTLIST definition-src + %stdAttrs; + %xlinkRefAttrs; + xlink:href %URI; #REQUIRED > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Metadata + ============================================================== --> + +<!ENTITY % metadataExt "" > +<!ELEMENT metadata (#PCDATA %metadataExt;)* > +<!ATTLIST metadata + %stdAttrs; > + + +<!-- ============================================================== + DECLARATIONS CORRESPONDING TO: Extensibility + ============================================================== --> + +<!ENTITY % foreignObjectExt "" > +<!ELEMENT foreignObject (#PCDATA %ceExt;%foreignObjectExt;)* > +<!ATTLIST foreignObject + %stdAttrs; + %testAttrs; + %langSpaceAttrs; + externalResourcesRequired %Boolean; #IMPLIED + class %ClassList; #IMPLIED + style %StyleSheet; #IMPLIED + %PresentationAttributes-All; + transform %TransformList; #IMPLIED + %graphicsElementEvents; + x %Coordinate; #IMPLIED + y %Coordinate; #IMPLIED + width %Length; #REQUIRED + height %Length; #REQUIRED + %StructuredText; > + +<!-- jDip-specific XML additions --> +<!-- PROVINCE_DATA --> +<!ENTITY % svgExt "| jdipNS:PROVINCE_DATA" > + +<!ELEMENT jdipNS:PROVINCE_DATA (jdipNS:PROVINCE)+> +<!ATTLIST jdipNS:PROVINCE_DATA + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + dislodgedUnitOffset CDATA #IMPLIED + > +<!ELEMENT jdipNS:PROVINCE (jdipNS:UNIT, jdipNS:DISLODGED_UNIT?, jdipNS:SUPPLY_CENTER?)+> +<!ATTLIST jdipNS:PROVINCE + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + name CDATA #REQUIRED + > +<!ELEMENT jdipNS:UNIT EMPTY> +<!ATTLIST jdipNS:UNIT + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + x CDATA #REQUIRED + y CDATA #REQUIRED + > +<!ELEMENT jdipNS:DISLODGED_UNIT EMPTY> +<!ATTLIST jdipNS:DISLODGED_UNIT + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + x CDATA #REQUIRED + y CDATA #REQUIRED + > +<!ELEMENT jdipNS:SUPPLY_CENTER EMPTY> +<!ATTLIST jdipNS:SUPPLY_CENTER + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + x CDATA #REQUIRED + y CDATA #REQUIRED + > + +<!-- Display Parameters --> +<!ENTITY % svgExt "| jdipNS:DISPLAY" > + +<!ELEMENT jdipNS:DISPLAY (jdipNS:ZOOM, jdipNS:LABELS)> +<!ATTLIST jdipNS:DISPLAY + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + > +<!ELEMENT jdipNS:ZOOM EMPTY> +<!ATTLIST jdipNS:ZOOM + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + min CDATA #REQUIRED + max CDATA #REQUIRED + factor CDATA #REQUIRED + > +<!ELEMENT jdipNS:LABELS EMPTY> +<!ATTLIST jdipNS:LABELS + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + brief (true|false) "true" + full (true|false) "true" + > + + <!-- Order Drawing Parameters --> +<!ENTITY % svgExt "| " > + +<!ELEMENT jdipNS:ORDERDRAWING (jdipNS:POWERCOLORS, jdipNS:SYMBOLSIZE+, jdipNS:BUILD, jdipNS:REMOVE, jdipNS:DISBAND, jdipNS:WAIVE, jdipNS:HOLD, jdipNS:MOVE, jdipNS:RETREAT, jdipNS:SUPPORT, jdipNS:CONVOY )> +<!ATTLIST jdipNS:ORDERDRAWING + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + > + +<!ELEMENT jdipNS:POWERCOLORS (jdipNS:POWERCOLOR+)> +<!ATTLIST jdipNS:POWERCOLORS + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + > + +<!ELEMENT jdipNS:POWERCOLOR EMPTY> +<!ATTLIST jdipNS:POWERCOLOR + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + power CDATA #REQUIRED + color CDATA #REQUIRED + > + +<!ELEMENT jdipNS:SYMBOLSIZE EMPTY> +<!ATTLIST jdipNS:SYMBOLSIZE + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + name CDATA #REQUIRED + width CDATA #REQUIRED + height CDATA #REQUIRED + > + + <!ELEMENT jdipNS:HOLD EMPTY> +<!ATTLIST jdipNS:HOLD + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + deltaRadius CDATA #REQUIRED + strokeCSSStyle CDATA #REQUIRED + filterID CDATA #IMPLIED + highlightOffset CDATA #REQUIRED + highlightCSSClass CDATA #REQUIRED + widths CDATA #REQUIRED + shadowWidths CDATA #REQUIRED + > + +<!ELEMENT jdipNS:DISBAND EMPTY> +<!ATTLIST jdipNS:DISBAND + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + deltaRadius CDATA #REQUIRED + > + +<!ELEMENT jdipNS:WAIVE EMPTY> +<!ATTLIST jdipNS:WAIVE + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + deltaRadius CDATA #REQUIRED +> + +<!ELEMENT jdipNS:REMOVE EMPTY> +<!ATTLIST jdipNS:REMOVE + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + deltaRadius CDATA #REQUIRED + > + +<!ELEMENT jdipNS:BUILD EMPTY> +<!ATTLIST jdipNS:BUILD + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + deltaRadius CDATA #REQUIRED + > + +<!ELEMENT jdipNS:MOVE EMPTY> +<!ATTLIST jdipNS:MOVE + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + deltaRadius CDATA #REQUIRED + strokeCSSStyle CDATA #REQUIRED + markerID CDATA #REQUIRED + filterID CDATA #IMPLIED + highlightOffset CDATA #REQUIRED + highlightCSSClass CDATA #REQUIRED + widths CDATA #REQUIRED + shadowWidths CDATA #REQUIRED + > + +<!ELEMENT jdipNS:RETREAT EMPTY> +<!ATTLIST jdipNS:RETREAT + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + deltaRadius CDATA #REQUIRED + strokeCSSStyle CDATA #REQUIRED + markerID CDATA #REQUIRED + filterID CDATA #IMPLIED + highlightOffset CDATA #REQUIRED + highlightCSSClass CDATA #REQUIRED + > + +<!ELEMENT jdipNS:SUPPORT EMPTY> +<!ATTLIST jdipNS:SUPPORT + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + deltaRadius CDATA #REQUIRED + strokeCSSStyle CDATA #REQUIRED + markerID CDATA #REQUIRED + filterID CDATA #IMPLIED + highlightOffset CDATA #REQUIRED + highlightCSSClass CDATA #REQUIRED + > + +<!ELEMENT jdipNS:CONVOY EMPTY> +<!ATTLIST jdipNS:CONVOY + xmlns:jdipNS CDATA #FIXED "http://jdip.sourceforge.org/jdipNS" + deltaRadius CDATA #REQUIRED + strokeCSSStyle CDATA #REQUIRED + markerID CDATA #REQUIRED + filterID CDATA #IMPLIED + highlightOffset CDATA #REQUIRED + highlightCSSClass CDATA #REQUIRED + >
\ No newline at end of file diff --git a/diplomacy/maps/tests/__init__.py b/diplomacy/maps/tests/__init__.py new file mode 100644 index 0000000..4f2769f --- /dev/null +++ b/diplomacy/maps/tests/__init__.py @@ -0,0 +1,16 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== diff --git a/diplomacy/maps/tests/test_map_gen.py b/diplomacy/maps/tests/test_map_gen.py new file mode 100644 index 0000000..9cf44d6 --- /dev/null +++ b/diplomacy/maps/tests/test_map_gen.py @@ -0,0 +1,35 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Test Map Generation + - Contains test for map generation +""" +import glob +import os +import sys + +from diplomacy.engine.map import Map + +MODULE_PATH = sys.modules['diplomacy'].__path__[0] + +def test_map_creation(): + """ Tests for map creation """ + maps = glob.glob(os.path.join(MODULE_PATH, 'diplomacy', 'maps', '*.map')) + for current_map in maps: + map_name = current_map[current_map.rfind('/') + 1:].replace('.map', '') + this_map = Map(map_name) + assert this_map.error == [], 'Map %s should have no errors' % map_name + this_map = None diff --git a/diplomacy/maps/world.map b/diplomacy/maps/world.map new file mode 100644 index 0000000..f4eda8c --- /dev/null +++ b/diplomacy/maps/world.map @@ -0,0 +1,520 @@ +BEGIN SPRING 2000 MOVEMENT + +ARGENTINA (ARGENTINIAN) BAI CHI SCR +A SCR +F BAI +F CHI + +BRAZIL (BRAZILIAN) BRA REC RDJ +A BRA +A RDJ +F REC + +CHINA (CHINESE) BEI GUA SHA +A BEI +A SHA +F GUA + +EUROPE (EUROPEAN) FRA GER ITA +A GER +F FRA/NC +F ITA + +FROZEN-ANTARCTICA (FROZEN) CAS LEN MAW +F CAS +F LEN +F MAW + +GHANA (GHANAN) GHA GUI MAL +A GHA +A MAL +F GUI + +INDIA (INDIAN) BOM CLC DEL +A CLC +A DEL +F BOM + +KENYA (KENYAN) UGA KEN TAN +A KEN +A UGA +F TAN + +LIBYA (LIBYAN) EGY LIB NSU +A LIB +A NSU +F EGY + +NEAR-EAST (EASTERN:N) IRQ SAR SYR +A IRQ +A SAR +A SYR + +PACIFIC-RUSSIA (PACIFIC) ESI VLA YAK +A ESI +A VLA +F YAK/SC + +QUEBEC (QUEBECER) ONT QBC NEW +A QBC +F NEW +F ONT + +RUSSIA (RUSSIAN) BEL MOS STP +A BEL +A MOS +A STP + +SOUTH-AFRICA (SOUTH-AFRICAN) NAM SAF SAN +A NAM +F SAN +F SAF + +UNITED-STATES (AMERICAN:U) CLF FLO TEX +A TEX +F CLF +F FLO + +WESTERN-CANADA (CANADIAN:W) BCO NWT YTE +A YTE +F BCO +F NWT + +OZ (AUSTRALIAN:Z) NSW VIC WAU +F NSW +F VIC +F WAU + +UNOWNED AFG ALA ALG ARM BAN BAT CHA COL CON ETH GBR GOD IND ITE JAP KOR LAN MAN +UNOWNED MEX MOR MOZ NZE PER SCA SHO SOP THA TRK UKR UNI URA VEN WSI ZAI ZAM + + +Afghanistan = AFG +Austria-Hungary = AHU +Alaska = ALA +Alberta = ALB +Algeria = ALG +Amazon = AMA +Angola = ANG +Arctic Circle = AOC +Antarctic Penninsula = APE +Armenia = ARM +Arabian Sea = ASE +Buenos Aires = BAI +Balkans = BAL +Balkans (East Coast) = BAL/EC +Balkans (South Coast) = BAL/SC +Bangladesh = BAN +Barents Sea = BAR +Baltic Sea = BAS +British Antarctic Territory = BAT +British Antarctic Territory (East Coast) = BAT/EC +British Antarctic Territory (West Coast) = BAT/WC +British Columbia = BCO +Beijing = BEI +Belorussia = BEL +Beaufort Sea = BFS +Belo Horizonte = BHO +Baffin Island = BIS +Black Sea = BLA +Bay of Bengal = BOB +Bolivia = BOL +Bombay = BOM +Botswana = BOT +Brasillia = BRA +Bering Strait = BRS +Baltic States = BST +Burma = BUR +Cameroon = CAM +Central African Republic = CAR +Casey = CAS +Central America = CEN +Chad = CHA +Chile = CHI +Carribean Islands = CIS +Calcutta = CLC +California = CLF +Colombia = COL +Colombia (North Coast) = COL/NC +Colombia (West Coast) = COL/WC +Congo = CON +Central Pacific Ocean = CPO +Carribean Sea = CSE +Delhi = DEL +Dumont dUrville = DMV +Deep South = DSO +East Atlantic Ocean = EAO +Egypt = EGY +East Pacific = EPA +East Siberia = ESI +East Siberian Sea = ESS +Ethiopia = ETH +Florida = FLO +France = FRA +France (North Coast) = FRA/NC +France (South Coast) = FRA/SC +Great Britain = GBR +Germany = GER +Province of Ghana = GHA +Godthab = GOD +Gulf of Mexico = GOM +Guangzhou = GUA +Guinea = GUI +Guyana = GUY +Hudson Bay = HBA +Heilongjiang = HEI +Inner Mongolia = IMO +Indonesia = IND +Iran = IRA +Irkutsk = IRK +Iraq = IRQ +Italy = ITA +Indian Territory = ITE +Japan = JAP +Java Sea = JSE +Kamchatka = KAM +Kazakhstan = KAZ +Province of Kenya = KEN +Korea = KOR +Kyrgyzstan = KYR +Lanzhou = LAN +Leningradskaya = LEN +Province of Libya = LIB +Labrador Sea = LSE +Madagascar = MAD +Mali = MAL +Manitoba = MAN +Mauritania = MAU +Mawson = MAW +Marie Byrd Land = MBL +Mozambique Channel = MCH +Mediterranean = MED +Mendoza = MEN +Mexico = MEX +Mexico (East Coast) = MEX/EC +Mexico (West Coast) = MEX/WC +Midwest = MID +Mongolia = MNG +Monterrey = MNT +Monterrey (East Coast) = MNT/EC +Monterrey (West Coast) = MNT/WC +Morocco = MOR +Moscow = MOS +Mozambique = MOZ +Namibia = NAM +Norwegian Dependency = NDE +Northeast Atlantic = NEA +Northeast Indian = NEI +Northeast Pacific = NEP +Nevada = NEV +NewfoundLand = NEW +Nigeria = NGA +Niger = NGR +Novolazarevskaya = NOV +North Pacific Ocean = NPO +North Sudan = NSU +New South Wales = NSW +Northern Territory = NTE +North Sea = NTH +Northwest Atlantic = NWA +Norwegian Sea = NWG +Northwest Indian = NWI +Northwest Pacific = NWP +Northwest Territories = NWT +New Zealand = NZE +Omsk = OMS +Ontario = ONT +Pakistan = PAK +Peru = PER +Phillipines = PHI +Pacific Islands = PIS +Poland = POL +Province of Quebec = QBC +Province of Quebec (North Coast) = QBC/NC +Province of Quebec (South Coast) = QBC/SC +Queensland = QSL +Rio de Janeiro = RDJ +Recife = REC +Ross Ice Shelf = RIS +Red Sea = RSE +South Africa = SAF +Salvador = SAL +Sanae IV = SAN +Sao Paulo = SAO +Saudi Arabia = SAR +Saudi Arabia (North Coast) = SAR/NC +Saudi Arabia (South Coast) = SAR/SC +Saskatchewan = SAS +South Australia = SAU +Skandanavia = SCA +Santa Cruz = SCR +South China Sea = SCS +Southeast Atlantic = SEA +Southeast Indian = SEI +Southeast Pacific = SEP +Shanghai = SHA +Syowa = SHO +Southern Indian Ocean = SIO +Sea of Japan = SOJ +Somalia = SOM +Sea of Othoksk = SOO +South Pole = SOP +Spain = SPA +South Pacific Ocean = SPO +South Sudan = SSU +Saint Petersburg = STP +Saint Petersburg (North Coast) = STP/NC +Saint Petersburg (South Coast) = STP/SC +Southwest Atlantic = SWA +Southwest Indian = SWI +Southwest Pacific = SWP +Syria = SYR +Tajikistan = TAJ +Tanzania = TAN +Texas = TEX +Thailand = THA +Thule = THU +Tibet = TIB +Turkmenistan = TKM +Turkey = TRK +Tasman Sea = TSE +Uganda = UGA +Ukraine = UKR +Union = UNI +Uruguay = URA +Uzbekistan = UZB +Venezuela = VEN +Victoria = VIC +Vietnam = VIE +Vladivostok = VLA +Vostok = VOS +West Atlantic Ocean = WAO +Western Australia = WAU +Western China = WCH +West Pacific Ocean = WPO +West Siberia = WSI +Yakutsk = YAK +Yakutsk (North Coast) = YAK/NC +Yakutsk (South Coast) = YAK/SC +Yellow Sea = YSE +Yukon Territory = YTE +Yukon Territory (North Coast) = YTE/NC +Yukon Territory (South Coast) = YTE/SC +Zaire = ZAI +Zambia = ZAM +Zimbabwe = ZIM + +LAND AFG ABUTS IRA PAK TAJ TKM UZB WCH +COAST AHU ABUTS BAL/SC ger ITA MED pol ukr +COAST ALA ABUTS AOC BFS BRS NPO YTE/NC YTE/SC +LAND ALB ABUTS BCO ITE NWT SAS +COAST ALG ABUTS LIB MAL mau MED MOR NEA NGR +LAND AMA ABUTS BOL BRA COL PER VEN +COAST ANG ABUTS CON EAO NAM ZAI ZAM +WATER AOC ABUTS ALA BAR BFS BRS ESS KAM YAK/NC +COAST APE ABUTS BAT/EC BAT/WC SEP SWA +COAST ARM ABUTS BLA ira MOS TRK +WATER ASE ABUTS BOM IRA IRQ NWI PAK RSE SAR/SC SOM +COAST BAI ABUTS MEN SCR SWA URA WAO +COAST bal ABUTS AHU BLA MED TRK UKR +COAST BAL/EC ABUTS BLA TRK UKR +COAST BAL/SC ABUTS AHU MED TRK +COAST BAN ABUTS BOB BUR CLC LAN +WATER BAR ABUTS AOC BFS ESS GOD NWG SCA STP/NC THU WSI +WATER BAS ABUTS BST GER NTH POL SCA STP/SC +COAST bat ABUTS APE MBL NDE SAN SEP SOP SWA +COAST BAT/EC ABUTS APE SAN SWA +COAST BAT/WC ABUTS APE MBL SEP +COAST BCO ABUTS ALB ITE NPO nwt YTE/SC +COAST BEI ABUTS HEI IMO KOR LAN SHA YSE +LAND BEL ABUTS BST MOS POL STP UKR +WATER BFS ABUTS ALA AOC BAR BIS HBA LSE NWT THU YTE/NC +LAND BHO ABUTS BRA RDJ SAL +COAST BIS ABUTS BFS HBA LSE +WATER BLA ABUTS ARM BAL/EC MOS TRK UKR +WATER BOB ABUTS BAN BOM BUR CLC NEI THA +LAND BOL ABUTS AMA BRA CHI MEN PER SAO +COAST BOM ABUTS ASE BOB CLC DEL NEI NWI PAK +LAND BOT ABUTS NAM SAF ZIM +LAND BRA ABUTS AMA BHO BOL GUY RDJ REC SAL SAO VEN +WATER BRS ABUTS ALA AOC KAM NPO NWP +COAST BST ABUTS BAS BEL POL STP/SC +COAST BUR ABUTS BAN BOB LAN THA vie +COAST CAM ABUTS CAR CHA CON EAO NGA +LAND CAR ABUTS CAM CHA CON SSU ZAI +COAST CAS ABUTS DMV SEI VOS +COAST CEN ABUTS COL/NC COL/WC CSE MEX/EC MEX/WC NEP +LAND CHA ABUTS CAM CAR LIB NGA NGR NSU SSU +COAST CHI ABUTS BOL EPA MEN PER SCR SEP SWA +COAST CIS ABUTS CSE GOM NWA +COAST CLC ABUTS BAN BOB BOM DEL LAN TIB +COAST CLF ABUTS ITE MNT/WC NEV NPO +COAST col ABUTS AMA CEN CSE NEP PER VEN +COAST COL/NC ABUTS CEN CSE VEN +COAST COL/WC ABUTS CEN NEP PER +COAST CON ABUTS ANG CAM CAR EAO ZAI +WATER CPO ABUTS NEP NPO NWP PIS WPO +WATER CSE ABUTS CEN CIS COL/NC GOM MEX/EC NWA VEN +LAND DEL ABUTS BOM CLC PAK TIB WCH +COAST DMV ABUTS CAS LEN NDE SEI vos +COAST DSO ABUTS FLO GOM MID NEV TEX uni +WATER EAO ABUTS ANG CAM CON GHA GUI MAU NAM NEA NGA SAF SEA SWA WAO +COAST EGY ABUTS LIB MED NSU RSE SAR/NC SAR/SC +WATER EPA ABUTS CHI NEP PER PIS SEP SPO +COAST ESI ABUTS ESS IRK OMS WSI YAK/NC +WATER ESS ABUTS AOC BAR ESI WSI YAK/NC +COAST ETH ABUTS ken NSU RSE SOM SSU +COAST FLO ABUTS DSO GOM NWA UNI +COAST fra ABUTS GER ITA MED NEA NTH SPA +COAST FRA/NC ABUTS GER NEA NTH SPA +COAST FRA/SC ABUTS ITA MED SPA +COAST GBR ABUTS NEA NTH NWG +COAST GER ABUTS ahu BAS FRA/NC ita NTH POL +COAST GHA ABUTS EAO GUI MAL NGA NGR +COAST GOD ABUTS BAR LSE NWG THU +WATER GOM ABUTS CIS CSE DSO FLO MEX/EC MNT/EC NWA TEX +COAST GUA ABUTS LAN SCS SHA VIE YSE +COAST GUI ABUTS EAO GHA MAL MAU +COAST GUY ABUTS BRA NWA REC VEN +WATER HBA ABUTS BFS BIS LSE MAN NEW NWT ONT QBC/NC +LAND HEI ABUTS BEI IMO KOR VLA +LAND IMO ABUTS BEI HEI IRK LAN MNG VLA WCH +COAST IND ABUTS JSE NEI PIS SCS WPO +COAST IRA ABUTS AFG arm ASE IRQ PAK TKM trk +LAND IRK ABUTS ESI IMO MNG OMS VLA YAK +COAST IRQ ABUTS ASE IRA SAR/SC syr trk +COAST ITA ABUTS AHU FRA/SC ger MED +COAST ITE ABUTS ALB BCO CLF MID NEV NPO SAS +COAST JAP ABUTS NWP SOJ SOO WPO YSE +WATER JSE ABUTS IND NEI NTE PIS QSL WAU +COAST KAM ABUTS AOC BRS NWP SOO YAK/NC YAK/SC +LAND KAZ ABUTS KYR MNG MOS OMS TKM UZB WCH WSI +COAST KEN ABUTS eth NWI SOM SSU TAN UGA +COAST KOR ABUTS BEI HEI SOJ VLA YSE +LAND KYR ABUTS KAZ TAJ UZB WCH +LAND LAN ABUTS BAN BEI BUR CLC GUA IMO SHA TIB VIE WCH +COAST LEN ABUTS DMV NDE RIS SEI SOP SWP +COAST LIB ABUTS ALG CHA EGY MED NGR nsu +WATER LSE ABUTS BFS BIS GOD HBA NEA NEW NWA NWG THU +COAST MAD ABUTS MCH NWI SWI +LAND MAL ABUTS ALG GHA GUI MAU NGR +COAST MAN ABUTS HBA MID NWT ONT SAS +COAST MAU ABUTS alg EAO GUI MAL MOR NEA +COAST MAW ABUTS NDE SHO SIO SWI VOS +COAST MBL ABUTS BAT/WC RIS SEP SOP SPO +WATER MCH ABUTS MAD MOZ NWI SAF SWI TAN +WATER MED ABUTS AHU ALG BAL/SC EGY FRA/SC ITA LIB NEA SAR/NC SPA SYR TRK +LAND MEN ABUTS BAI BOL CHI SAO SCR URA +COAST mex ABUTS CEN CSE GOM MNT NEP +COAST MEX/EC ABUTS CEN CSE GOM MNT/EC +COAST MEX/WC ABUTS CEN MNT/WC NEP +LAND MID ABUTS DSO ITE MAN NEV ONT SAS UNI +LAND MNG ABUTS IMO IRK KAZ OMS WCH +COAST mnt ABUTS CLF GOM MEX NEP NEV NPO TEX +COAST MNT/EC ABUTS GOM MEX/EC TEX +COAST MNT/WC ABUTS CLF MEX/WC NEP NPO +COAST MOR ABUTS ALG MAU NEA +COAST MOS ABUTS ARM BEL BLA KAZ stp UKR wsi +COAST MOZ ABUTS MCH SAF TAN ZAM ZIM +COAST NAM ABUTS ANG BOT EAO SAF ZAM ZIM +LAND NDE ABUTS BAT DMV LEN MAW NOV SAN SHO SOP VOS +WATER NEA ABUTS ALG EAO FRA/NC GBR LSE MAU MED MOR NTH NWA NWG SPA WAO +WATER NEI ABUTS BOB BOM IND JSE NWI SCS SEI SIO THA WAU +WATER NEP ABUTS CEN COL/WC CPO EPA MEX/WC MNT/WC NPO PER PIS +LAND NEV ABUTS CLF DSO ITE MID MNT TEX +COAST NEW ABUTS HBA LSE NWA QBC/NC QBC/SC +COAST NGA ABUTS CAM CHA EAO GHA NGR +LAND NGR ABUTS ALG CHA GHA LIB MAL NGA +COAST NOV ABUTS NDE SAN SEA SHO +WATER NPO ABUTS ALA BCO BRS CLF CPO ITE MNT/WC NEP NWP YTE/SC +COAST NSU ABUTS CHA EGY ETH lib RSE SSU +COAST NSW ABUTS QSL sau TSE VIC +COAST NTE ABUTS JSE QSL sau WAU +WATER NTH ABUTS BAS FRA/NC GBR GER NEA NWG SCA +WATER NWA ABUTS CIS CSE FLO GOM GUY LSE NEA NEW QBC/SC REC UNI VEN WAO +WATER NWG ABUTS BAR GBR GOD LSE NEA NTH SCA +WATER NWI ABUTS ASE BOM KEN MAD MCH NEI SIO SOM SWI TAN +WATER NWP ABUTS BRS CPO JAP KAM NPO SOO WPO +COAST NWT ABUTS ALB bco BFS HBA MAN SAS YTE/NC +COAST NZE ABUTS PIS SWP TSE +LAND OMS ABUTS ESI IRK KAZ MNG WSI +COAST ONT ABUTS HBA MAN MID QBC/NC uni +COAST PAK ABUTS AFG ASE BOM DEL IRA WCH +COAST PER ABUTS AMA BOL CHI COL/WC EPA NEP +COAST PHI ABUTS SCS WPO YSE +WATER PIS ABUTS CPO EPA IND JSE NEP NZE QSL SPO SWP TSE WPO +COAST POL ABUTS ahu BAS BEL BST GER ukr +COAST qbc ABUTS HBA NEW NWA ONT UNI +COAST QBC/NC ABUTS HBA NEW ONT +COAST QBC/SC ABUTS NEW NWA UNI +COAST QSL ABUTS JSE NSW NTE PIS sau TSE +COAST RDJ ABUTS BHO BRA SAL SAO WAO +COAST REC ABUTS BRA GUY NWA SAL WAO +COAST RIS ABUTS LEN MBL SOP SPO SWP +WATER RSE ABUTS ASE EGY ETH NSU SAR/SC SOM +COAST SAF ABUTS BOT EAO MCH MOZ NAM SEA SWI ZIM +COAST SAL ABUTS BHO BRA RDJ REC WAO +COAST SAN ABUTS BAT/EC NDE NOV SEA SWA +COAST SAO ABUTS BOL BRA MEN RDJ URA WAO +COAST sar ABUTS ASE EGY IRQ MED RSE SYR +COAST SAR/NC ABUTS EGY MED SYR +COAST SAR/SC ABUTS ASE EGY IRQ RSE +LAND SAS ABUTS ALB ITE MAN MID NWT +COAST SAU ABUTS nsw nte qsl SEI VIC WAU +COAST SCA ABUTS BAR BAS NTH NWG STP/NC STP/SC +COAST SCR ABUTS BAI CHI MEN SWA +WATER SCS ABUTS GUA IND NEI PHI THA VIE WPO YSE +WATER SEA ABUTS EAO NOV SAF SAN SHO SWA SWI +WATER SEI ABUTS CAS DMV LEN NEI SAU SIO SWP VIC VOS WAU +WATER SEP ABUTS APE BAT/WC CHI EPA MBL SPO SWA +COAST SHA ABUTS BEI GUA LAN YSE +COAST SHO ABUTS MAW NDE NOV SEA SWI +WATER SIO ABUTS MAW NEI NWI SEI SWI VOS +WATER SOJ ABUTS JAP KOR SOO VLA YSE +COAST SOM ABUTS ASE ETH KEN NWI RSE +WATER SOO ABUTS JAP KAM NWP SOJ VLA YAK/SC +LAND SOP ABUTS BAT LEN MBL NDE RIS +COAST SPA ABUTS FRA/NC FRA/SC MED NEA +WATER SPO ABUTS EPA MBL PIS RIS SEP SWP +LAND SSU ABUTS CAR CHA ETH KEN NSU UGA ZAI +COAST stp ABUTS BAR BAS BEL BST MOS SCA WSI +COAST STP/NC ABUTS BAR SCA WSI +COAST STP/SC ABUTS BAS BST SCA +WATER SWA ABUTS APE BAI BAT/EC CHI EAO SAN SCR SEA SEP WAO +WATER SWI ABUTS MAD MAW MCH NWI SAF SEA SHO SIO +WATER SWP ABUTS LEN NZE PIS RIS SEI SPO TSE VIC +COAST SYR ABUTS irq MED SAR/NC TRK +LAND TAJ ABUTS AFG KYR UZB WCH +COAST TAN ABUTS KEN MCH MOZ NWI UGA ZAI ZAM +COAST TEX ABUTS DSO GOM MNT/EC NEV +COAST THA ABUTS BOB BUR NEI SCS VIE +COAST THU ABUTS BAR BFS GOD LSE +LAND TIB ABUTS CLC DEL LAN WCH +LAND TKM ABUTS AFG IRA KAZ UZB +COAST TRK ABUTS ARM BAL/EC BAL/SC BLA ira irq MED SYR +WATER TSE ABUTS NSW NZE PIS QSL SWP VIC +LAND UGA ABUTS KEN SSU TAN ZAI +COAST UKR ABUTS ahu BAL/EC BEL BLA MOS pol +COAST UNI ABUTS dso FLO MID NWA ont QBC/SC +COAST URA ABUTS BAI MEN SAO WAO +LAND UZB ABUTS AFG KAZ KYR TAJ TKM +COAST VEN ABUTS AMA BRA COL/NC CSE GUY NWA +COAST VIC ABUTS NSW SAU SEI SWP TSE +COAST VIE ABUTS bur GUA LAN SCS THA +COAST VLA ABUTS HEI IMO IRK KOR SOJ SOO YAK/SC +COAST VOS ABUTS CAS dmv MAW NDE SEI SIO +WATER WAO ABUTS BAI EAO NEA NWA RDJ REC SAL SAO SWA URA +COAST WAU ABUTS JSE NEI NTE SAU SEI +LAND WCH ABUTS AFG DEL IMO KAZ KYR LAN MNG PAK TAJ TIB +WATER WPO ABUTS CPO IND JAP NWP PHI PIS SCS YSE +COAST WSI ABUTS BAR ESI ESS KAZ mos OMS STP/NC +COAST yak ABUTS AOC ESI ESS IRK KAM SOO VLA +COAST YAK/NC ABUTS AOC ESI ESS KAM +COAST YAK/SC ABUTS KAM SOO VLA +WATER YSE ABUTS BEI GUA JAP KOR PHI SCS SHA SOJ WPO +COAST yte ABUTS ALA BCO BFS NPO NWT +COAST YTE/NC ABUTS ALA BFS NWT +COAST YTE/SC ABUTS ALA BCO NPO +LAND ZAI ABUTS ANG CAR CON SSU TAN UGA ZAM +LAND ZAM ABUTS ANG MOZ NAM TAN ZAI ZIM +LAND ZIM ABUTS BOT MOZ NAM SAF ZAM diff --git a/diplomacy/server/__init__.py b/diplomacy/server/__init__.py new file mode 100644 index 0000000..acc0ee4 --- /dev/null +++ b/diplomacy/server/__init__.py @@ -0,0 +1,16 @@ +# ============================================================================== +# 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/>. +# ============================================================================== diff --git a/diplomacy/server/connection_handler.py b/diplomacy/server/connection_handler.py new file mode 100644 index 0000000..0089db3 --- /dev/null +++ b/diplomacy/server/connection_handler.py @@ -0,0 +1,111 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Tornado connection handler class, used internally to manage data received by server application. """ +import logging + +from urllib.parse import urlparse +from tornado import gen +from tornado.websocket import WebSocketHandler, WebSocketClosedError + +import ujson as json + +from diplomacy.communication import responses, requests +from diplomacy.server import request_managers +from diplomacy.utils import exceptions, strings + + +LOGGER = logging.getLogger(__name__) + +class ConnectionHandler(WebSocketHandler): + """ ConnectionHandler class. Properties: + - server: server object representing running server. + """ + # pylint: disable=abstract-method + + def __init__(self, *args, **kwargs): + self.server = None + super(ConnectionHandler, self).__init__(*args, **kwargs) + + def initialize(self, server=None): + """ Initialize the connection handler. + :param server: a Server object. + :type server: diplomacy.Server + """ + # pylint: disable=arguments-differ + if self.server is None: + self.server = server + + def get_compression_options(self): + """ Return compression options for the connection (see parent method). + Non-None enables compression with default options. + """ + return {} + + def check_origin(self, origin): + """ Return True if we should accept connexion from given origin (str). """ + + # It seems origin may be 'null', e.g. if client is a web page loaded from disk (`file:///my_test_file.html`). + # Accept it. + if origin == 'null': + return True + + # Try to check if origin matches host (without regarding port). + # Adapted from parent method code (tornado 4.5.3). + parsed_origin = urlparse(origin) + origin = parsed_origin.netloc.split(':')[0] + origin = origin.lower() + # Split host with ':' and keep only first piece to ignore eventual port. + host = self.request.headers.get("Host").split(':')[0] + return origin == host + + def on_close(self): + """ Invoked when the socket is closed (see parent method). + Detach this connection handler from server users. + """ + self.server.users.remove_connection(self, remove_tokens=False) + LOGGER.info("Removed connection. Remaining %d connection(s).", self.server.users.count_connections()) + + @gen.coroutine + def on_message(self, message): + """ Parse given message and manage parsed data (expected a string representation of a request). """ + try: + json_request = json.loads(message) + if not isinstance(json_request, dict): + raise ValueError("Unable to convert a JSON string to a dictionary.") + except ValueError as exc: + # Error occurred because either message is not a JSON string or parsed JSON object is not a dict. + response = responses.Error(message='%s/%s' % (type(exc).__name__, str(exc))) + else: + try: + request = requests.parse_dict(json_request) + + if request.level is not None: + # Link request token to this connection handler. + self.server.users.attach_connection_handler(request.token, self) + + response = yield request_managers.handle_request(self.server, request, self) + if response is None: + response = responses.Ok(request_id=request.request_id) + + except exceptions.ResponseException as exc: + response = responses.Error(message='%s/%s' % (type(exc).__name__, exc.message), + request_id=json_request.get(strings.REQUEST_ID, None)) + + try: + yield self.write_message(response.json()) + except WebSocketClosedError: + LOGGER.error('Websocket is closed.') diff --git a/diplomacy/server/notifier.py b/diplomacy/server/notifier.py new file mode 100644 index 0000000..1c05335 --- /dev/null +++ b/diplomacy/server/notifier.py @@ -0,0 +1,333 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Server notifier class. Used to send server notifications, allowing to ignore some addresses. """ +from tornado import gen + +from diplomacy.communication import notifications +from diplomacy.utils import strings + +class Notifier(): + """ Server notifier class. """ + __slots__ = ['server', 'ignore_tokens', 'ignore_addresses'] + + def __init__(self, server, ignore_tokens=None, ignore_addresses=None): + """ Initialize a server notifier. You can specify some tokens or addresses to ignore using + ignore_tokens or ignore_addresses. Note that these parameters are mutually exclusive + (you can use either none of them or only one of them). + :param server: a server object. + :param ignore_tokens: (optional) sequence of tokens to ignore. + :param ignore_addresses: (optional) sequence of couples (power name, token) to ignore. + :type server: diplomacy.Server + """ + self.server = server + self.ignore_tokens = None + self.ignore_addresses = None + if ignore_tokens and ignore_addresses: + raise AssertionError('Notifier cannot ignore both tokens and addresses.') + + # Expect a sequence of tokens to ignore. + # Convert it to a set. + elif ignore_tokens: + self.ignore_tokens = set(ignore_tokens) + + # Expect a sequence of tuples (power name, token) to ignore. + # Convert it to a dict {power name => {token}} (each power name with all associated ignored tokens). + elif ignore_addresses: + self.ignore_addresses = {} + for power_name, token in ignore_addresses: + if power_name not in self.ignore_addresses: + self.ignore_addresses[power_name] = set() + self.ignore_addresses[power_name].add(token) + + def ignores(self, notification): + """ Return True if given notification must be ignored. + :param notification: + :return: a boolean + :type notification: notifications._AbstractNotification | notifications._GameNotification + """ + if self.ignore_tokens: + return notification.token in self.ignore_tokens + if self.ignore_addresses and notification.level == strings.GAME: + # We can ignore addresses only for game requests (as other requests only have a token, not a full address). + return (notification.game_role in self.ignore_addresses + and notification.token in self.ignore_addresses[notification.game_role]) + return False + + @gen.coroutine + def _notify(self, notification): + """ Register a notification to send. + :param notification: a notification instance. + :type notification: notifications._AbstractNotification | notifications._GameNotification + """ + connection_handler = self.server.users.get_connection_handler(notification.token) + if not self.ignores(notification) and connection_handler: + yield self.server.notifications.put((connection_handler, notification)) + + @gen.coroutine + def _notify_game(self, server_game, notification_class, **kwargs): + """ Send a game notification. + Game token, game ID and game role will be automatically provided to notification object. + :param server_game: game to notify + :param notification_class: class of notification to send + :param kwargs: (optional) other notification parameters + :type server_game: diplomacy.server.server_game.ServerGame + """ + for game_role, token in server_game.get_reception_addresses(): + yield self._notify(notification_class(token=token, + game_id=server_game.game_id, + game_role=game_role, + **kwargs)) + + @gen.coroutine + def _notify_power(self, game_id, power, notification_class, **kwargs): + """ Send a notification to all tokens of a power. + Automatically add token, game ID and game role to notification parameters. + :param game_id: power game ID. + :param power: power to send notification. + :param notification_class: class of notification to send. + :param kwargs: (optional) other notification parameters. + :type power: diplomacy.Power + """ + for token in power.tokens: + yield self._notify(notification_class(token=token, + game_id=game_id, + game_role=power.name, + **kwargs)) + + @gen.coroutine + def notify_game_processed(self, server_game, previous_phase_data, current_phase_data): + """ Notify all game tokens about a game phase update (game processing). + :param server_game: game to notify + :param previous_phase_data: game phase data before phase update + :param current_phase_data: game phase data after phase update + :type server_game: diplomacy.server.server_game.ServerGame + :type previous_phase_data: diplomacy.utils.game_phase_data.GamePhaseData + :type current_phase_data: diplomacy.utils.game_phase_data.GamePhaseData + """ + # Send game updates to observers ans omniscient observers.. + for game_role, token in server_game.get_observer_addresses(): + yield self._notify(notifications.GameProcessed( + token=token, + game_id=server_game.game_id, + game_role=game_role, + previous_phase_data=server_game.filter_phase_data(previous_phase_data, strings.OBSERVER_TYPE, False), + current_phase_data=server_game.filter_phase_data(current_phase_data, strings.OBSERVER_TYPE, True) + )) + for game_role, token in server_game.get_omniscient_addresses(): + yield self._notify(notifications.GameProcessed( + token=token, + game_id=server_game.game_id, + game_role=game_role, + previous_phase_data=server_game.filter_phase_data(previous_phase_data, strings.OMNISCIENT_TYPE, False), + current_phase_data=server_game.filter_phase_data(current_phase_data, strings.OMNISCIENT_TYPE, True))) + # Send game updates to powers. + for power in server_game.powers.values(): + yield self._notify_power(server_game.game_id, power, notifications.GameProcessed, + previous_phase_data=server_game.filter_phase_data( + previous_phase_data, power.name, False), + current_phase_data=server_game.filter_phase_data( + current_phase_data, power.name, True)) + + @gen.coroutine + def notify_account_deleted(self, username): + """ Notify all tokens of given username about account deleted. """ + for token_to_notify in self.server.users.get_tokens(username): + yield self._notify(notifications.AccountDeleted(token=token_to_notify)) + + @gen.coroutine + def notify_game_deleted(self, server_game): + """ Notify all game tokens about game deleted. + :param server_game: game to notify + :type server_game: diplomacy.server.server_game.ServerGame + """ + yield self._notify_game(server_game, notifications.GameDeleted) + + @gen.coroutine + def notify_game_powers_controllers(self, server_game): + """ Notify all game tokens about current game powers controllers. + :param server_game: game to notify + :type server_game: diplomacy.server.server_game.ServerGame + """ + yield self._notify_game(server_game, notifications.PowersControllers, + powers=server_game.get_controllers(), + timestamps=server_game.get_controllers_timestamps()) + + @gen.coroutine + def notify_game_status(self, server_game): + """ Notify all game tokens about current game status. + :param server_game: game to notify + :type server_game: diplomacy.server.server_game.ServerGame + """ + yield self._notify_game(server_game, notifications.GameStatusUpdate, status=server_game.status) + + @gen.coroutine + def notify_game_phase_data(self, server_game): + """ Notify all game tokens about current game state. + :param server_game: game to notify + :type server_game: diplomacy.server.server_game.ServerGame + """ + phase_data = server_game.get_phase_data() + state_type = strings.STATE + # Notify omniscient tokens. + yield self.notify_game_addresses(server_game.game_id, + server_game.get_omniscient_addresses(), + notifications.GamePhaseUpdate, + phase_data=server_game.filter_phase_data( + phase_data, strings.OMNISCIENT_TYPE, is_current=True), + phase_data_type=state_type) + # Notify observer tokens. + yield self.notify_game_addresses(server_game.game_id, + server_game.get_observer_addresses(), + notifications.GamePhaseUpdate, + phase_data=server_game.filter_phase_data( + phase_data, strings.OBSERVER_TYPE, is_current=True), + phase_data_type=state_type) + # Notify power addresses. + for power_name in server_game.get_map_power_names(): + yield self.notify_game_addresses(server_game.game_id, + server_game.get_power_addresses(power_name), + notifications.GamePhaseUpdate, + phase_data=server_game.filter_phase_data( + phase_data, power_name, is_current=True), + phase_data_type=state_type) + + @gen.coroutine + def notify_game_vote_updated(self, server_game): + """ Notify all game tokens about current game vote. + Send relevant notifications to each type of tokens. + :param server_game: game to notify + :type server_game: diplomacy.server.server_game.ServerGame + """ + # Notify observers about vote count changed. + for game_role, token in server_game.get_observer_addresses(): + yield self._notify(notifications.VoteCountUpdated(token=token, + game_id=server_game.game_id, + game_role=game_role, + count_voted=server_game.count_voted(), + count_expected=server_game.count_controlled_powers())) + # Notify omniscient observers about power vote changed. + for game_role, token in server_game.get_omniscient_addresses(): + yield self._notify(notifications.VoteUpdated(token=token, + game_id=server_game.game_id, + game_role=game_role, + vote={power.name: power.vote + for power in server_game.powers.values()})) + # Notify each power about its own changes. + for power in server_game.powers.values(): + yield self._notify_power(server_game.game_id, power, notifications.PowerVoteUpdated, + count_voted=server_game.count_voted(), + count_expected=server_game.count_controlled_powers(), + vote=power.vote) + + @gen.coroutine + def notify_power_orders_update(self, server_game, power, orders): + """ Notify all power tokens and all observers about new orders for given power. + :param server_game: game to notify + :param power: power to notify + :param orders: new power orders + :type server_game: diplomacy.server.server_game.ServerGame + :type power: diplomacy.Power + """ + yield self._notify_power(server_game.game_id, power, notifications.PowerOrdersUpdate, + power_name=power.name, orders=orders) + addresses = list(server_game.get_omniscient_addresses()) + list(server_game.get_observer_addresses()) + yield self.notify_game_addresses(server_game.game_id, addresses, + notifications.PowerOrdersUpdate, + power_name=power.name, orders=orders) + other_powers_addresses = [] + for other_power_name in server_game.powers: + if other_power_name != power.name: + other_powers_addresses.extend(server_game.get_power_addresses(other_power_name)) + yield self.notify_game_addresses(server_game.game_id, other_powers_addresses, + notifications.PowerOrdersFlag, + power_name=power.name, order_is_set=power.order_is_set) + + @gen.coroutine + def notify_power_wait_flag(self, server_game, power, wait_flag): + """ Notify all power tokens about new wait flag for given power. + :param server_game: game to notify + :param power: power to notify + :param wait_flag: new wait flag + :type power: diplomacy.Power + """ + yield self._notify_game(server_game, notifications.PowerWaitFlag, power_name=power.name, wait=wait_flag) + + @gen.coroutine + def notify_cleared_orders(self, server_game, power_name): + """ Notify all game tokens about game orders cleared for a given power name. + :param server_game: game to notify + :param power_name: name of power for which orders were cleared. + None means all power orders were cleared. + :type server_game: diplomacy.server.server_game.ServerGame + """ + yield self._notify_game(server_game, notifications.ClearedOrders, power_name=power_name) + + @gen.coroutine + def notify_cleared_units(self, server_game, power_name): + """ Notify all game tokens about game units cleared for a given power name. + :param server_game: game to notify + :param power_name: name of power for which units were cleared. + None means all power units were cleared. + :type server_game: diplomacy.server.server_game.ServerGame + """ + yield self._notify_game(server_game, notifications.ClearedUnits, power_name=power_name) + + @gen.coroutine + def notify_cleared_centers(self, server_game, power_name): + """ Notify all game tokens about game centers cleared for a given power name. + :param server_game: game to notify + :param power_name: name of power for which centers were cleared. + None means all power centers were cleared. + :type server_game: diplomacy.server.server_game.ServerGame + """ + yield self._notify_game(server_game, notifications.ClearedCenters, power_name=power_name) + + @gen.coroutine + def notify_game_message(self, server_game, game_message): + """ Notify relevant users about a game message received. + :param server_game: Game data who handles this game message. + :param game_message: the game message received. + :return: None + :type server_game: diplomacy.server.server_game.ServerGame + """ + if game_message.is_global(): + yield self._notify_game(server_game, notifications.GameMessageReceived, message=game_message) + else: + power_from = server_game.get_power(game_message.sender) + power_to = server_game.get_power(game_message.recipient) + yield self._notify_power( + server_game.game_id, power_from, notifications.GameMessageReceived, message=game_message) + yield self._notify_power( + server_game.game_id, power_to, notifications.GameMessageReceived, message=game_message) + for game_role, token in server_game.get_omniscient_addresses(): + yield self._notify(notifications.GameMessageReceived(token=token, + game_id=server_game.game_id, + game_role=game_role, + message=game_message)) + + @gen.coroutine + def notify_game_addresses(self, game_id, addresses, notification_class, **kwargs): + """ Notify addresses of a game with a notification. + Game ID is automatically provided to notification. + Token and game role are automatically provided to notifications from given addresses. + :param game_id: related game ID + :param addresses: addresses to notify. Sequence of couples (game role, token). + :param notification_class: class of notification to send + :param kwargs: (optional) other parameters for notification + """ + for game_role, token in addresses: + yield self._notify(notification_class(token=token, game_id=game_id, game_role=game_role, **kwargs)) diff --git a/diplomacy/server/request_manager_utils.py b/diplomacy/server/request_manager_utils.py new file mode 100644 index 0000000..9ea8264 --- /dev/null +++ b/diplomacy/server/request_manager_utils.py @@ -0,0 +1,267 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Utility classes and functions used for request management. + Put here to avoid having file request_managers.py with too many lines. +""" +from collections.__init__ import namedtuple + +from diplomacy.communication import notifications +from diplomacy.server.notifier import Notifier + +from diplomacy.utils import strings, exceptions + +class SynchronizedData(namedtuple('SynchronizedData', ('timestamp', 'order', 'type', 'data'))): + """ Small class used to store and sort data to synchronize for a game. Properties: + - timestamp (int): timestamp of related data to synchronize. + - order (int): rank of data to synchronize. + - type (str): type name of data to synchronize. Possible values: + - 'message': data is a game message. Order is 0. + - 'state_history': data is a game state for history. Order is 1. + - 'state': data is current game state. Order is 2. + - data: proper data to synchronize. + Synchronized data are sorted using timestamp then order, meaning that: + - data are synchronized from former to later timestamps + - for a same timestamp, messages are synchronized first, then states for history, then current state. + """ + +class GameRequestLevel(): + """ Describe a game level retrieved from a game request. Used by some game requests managers + to determine user rights in a game. Possible game levels: power, observer, omniscient and master. + """ + __slots__ = ['game', 'power_name', '__action_level'] + + def __init__(self, game, action_level, power_name): + """ Initialize a game request level. + :param game: related game data + :param action_level: action level, either: + - 'power' + - 'observer' + - 'omniscient' + - 'master' + :param power_name: (optional) power name specified in game request. Required if level is 'power'. + :type game: diplomacy.server.server_game.ServerGame + :type action_level: str + :type power_name: str + """ + assert action_level in {'power', 'observer', 'omniscient', 'master'} + self.game = game + self.power_name = power_name # type: str + self.__action_level = action_level # type: str + + def is_power(self): + """ Return True if game level is power. """ + return self.__action_level == 'power' + + def is_observer(self): + """ Return True if game level is observer. """ + return self.__action_level == 'observer' + + def is_omniscient(self): + """ Return True if game level is omniscient. """ + return self.__action_level == 'omniscient' + + def is_master(self): + """ Return True if game level is master. """ + return self.__action_level == 'master' + + @classmethod + def power_level(cls, game, power_name): + """ Create and return a game power level with given game data and power name. """ + return cls(game, 'power', power_name) + + @classmethod + def observer_level(cls, game, power_name): + """ Create and return a game observer level with given game data and power name. """ + return cls(game, 'observer', power_name) + + @classmethod + def omniscient_level(cls, game, power_name): + """ Create and return a game omniscient level with given game data and power name. """ + return cls(game, 'omniscient', power_name) + + @classmethod + def master_level(cls, game, power_name): + """ Create and return a game master level with given game data and power name. """ + return cls(game, 'master', power_name) + +def verify_request(server, request, connection_handler, + omniscient_role=True, observer_role=True, power_role=True, require_power=False, require_master=True): + """ Verify request token, and game role and rights if request is a game request. + Ignore connection requests (e.g. SignIn), as such requests don't have any token. + Verifying token: + - check if server knows request token + - check if request token is still valid. + - Update token lifetime. See method Server.assert_token() for more details. + Verifying game role and rights: + - check if server knows request game ID. + - check if request token is allowed to have request game role in associated game ID. + If request is a game request, return a GameRequestLevel containing: + - the server game object + - the level of rights (power, observer or master) allowed for request sender. + - the power name associated to request (if present), representing which power is queried by given request. + See class GameRequestLevel for more details. + :param server: server which receives the request + :param request: request received by server + :param connection_handler: connection handler which receives the request + :param omniscient_role: (for game requests) Indicate if omniscient role is accepted for this request. + :param observer_role: (for game requests) Indicate if observer role is accepted for this request. + :param power_role: (for game requests) Indicate if power role is accepted for this request. + :param require_power: (for game requests) Indicate if a power name is required for this request. + If true, either game role must be power role, or request must have a non-null `power_name` request. + :param require_master: (for game requests) Indicate if an omniscient must be a master. + If true and if request role is omniscient, then request token must be a master token for related game. + :return: a GameRequestLevel object for game requests, else None. + :rtype: diplomacy.server.request_manager_utils.GameRequestLevel + :type server: diplomacy.Server + :type request: requests._AbstractRequest | requests._AbstractGameRequest + :type connection_handler: diplomacy.server.connection_handler.ConnectionHandler + """ + + # A request may be a connection request, a channel request or a game request. + # For connection request, field level is None. + # For channel request, field level is CHANNEL. Channel request has a `token` field. + # For game request, field level is GAME. Game request is a channel request with supplementary fields + # `game_role` and `game_id`. + + # No permissions to check for connection requests (e.g. SignIn). + if not request.level: + return None + + # Check token for channel and game requests. + server.assert_token(request.token, connection_handler) + + # No more permissions to check for non-game requests. + if request.level != strings.GAME: + return None + + # Check and get game. + server_game = server.get_game(request.game_id) + + power_name = getattr(request, 'power_name', None) + + if strings.role_is_special(request.game_role): + + if request.game_role == strings.OMNISCIENT_TYPE: + + # Check if omniscient role is accepted (for this call). + if not omniscient_role: + raise exceptions.ResponseException( + 'Omniscient role disallowed for request %s' % request.name) + + # Check if request token is known as omniscient token by related game. + if not server_game.has_omniscient_token(request.token): + raise exceptions.GameTokenException() + + # Check if request token is a master token (if required for this call) + # and create game request level. + token_is_master = server.token_is_master(request.token, server_game) + if require_master and not token_is_master: + raise exceptions.GameMasterTokenException() + if token_is_master: + level = GameRequestLevel.master_level(server_game, power_name) + else: + level = GameRequestLevel.omniscient_level(server_game, power_name) + + else: + # Check if observer role is accepted (for this call). + if not observer_role: + raise exceptions.ResponseException( + 'Observer role disallowed for request %s' % request.game_role) + + # Check if request token is known as observer token by related game. + if not server_game.has_observer_token(request.token): + raise exceptions.GameTokenException() + + # Create game request level object. + level = GameRequestLevel.observer_level(server_game, power_name) + + # Check if we have a valid power name if power name is required (for this call) or given. + if power_name is None: + if require_power: + raise exceptions.MapPowerException(None) + elif not server_game.has_power(power_name): + raise exceptions.MapPowerException(power_name) + + else: + # Check if power role is accepted (for this call). + if not power_role: + raise exceptions.ResponseException('Power role disallowed for request %s' % request.name) + + # Get power name to check: either given power name if defined, else game role. + if power_name is None: + power_name = request.game_role + + # Check if given power name is valid. + if not server_game.has_power(power_name): + raise exceptions.MapPowerException(power_name) + + # Check if request sender is allowed to query given power name. + # We don't care anymore if sender token is currently associated to this power, + # as long as sender is registered as the controller of this power. + if not server_game.is_controlled_by(power_name, server.users.get_name(request.token)): + raise exceptions.ResponseException('User %s does not currently control power %s' + % (server.users.get_name(request.token), power_name)) + + # Create game request level. + level = GameRequestLevel.power_level(server_game, power_name) + + return level + +def transfer_special_tokens(server_game, server, username, grade_update, from_observation=True): + """ Transfer tokens of given username from an observation role to the opposite in given server game, + and notify all user tokens about observation role update with given grade update. + This method is used in request manager on_set_grade(). + :param server_game: server game in which tokens roles must be changed. + :param server: server from which notifications will be sent. + :param username: name of user whom tokens will be transferred. Only user tokens registered in + server games as observer tokens or omniscient tokens will be updated. + :param grade_update: type of upgrading. Possibles values in strings.ALL_GRADE_UPDATES (PROMOTE or DEMOTE). + :param from_observation: indicate transfer direction. + If True, we expect to transfer role from observer to omniscient. + If False, we expect to transfer role from omniscient to observer. + :type server_game: diplomacy.server.server_game.ServerGame + :type server: diplomacy.Server + """ + if from_observation: + old_role = strings.OBSERVER_TYPE + new_role = strings.OMNISCIENT_TYPE + token_filter = server_game.has_observer_token + else: + old_role = strings.OMNISCIENT_TYPE + new_role = strings.OBSERVER_TYPE + token_filter = server_game.has_omniscient_token + + connected_user_tokens = [user_token for user_token in server.users.get_tokens(username) if token_filter(user_token)] + + if connected_user_tokens: + + # Update observer level for each connected user token. + for user_token in connected_user_tokens: + server_game.transfer_special_token(user_token) + + addresses = [(old_role, user_token) for user_token in connected_user_tokens] + Notifier(server).notify_game_addresses( + server_game.game_id, addresses, notifications.OmniscientUpdated, + grade_update=grade_update, game=server_game.cast(new_role, username, server.users.has_admin(username))) + +def assert_game_not_finished(server_game): + """ Check if given game is not yet completed or canceled, otherwise raise a GameFinishedException. + :param server_game: server game to check + :type server_game: diplomacy.server.server_game.ServerGame + """ + if server_game.is_game_completed or server_game.is_game_canceled: + raise exceptions.GameFinishedException() diff --git a/diplomacy/server/request_managers.py b/diplomacy/server/request_managers.py new file mode 100644 index 0000000..d17a77b --- /dev/null +++ b/diplomacy/server/request_managers.py @@ -0,0 +1,1181 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Request managers (server side). Remarks: + Even if request managers use many server methods which are coroutines, we currently never yield + on any of this method because we don't need to wait for them to finish before continuing request + management. Thus, current request managers are all normal functions. + Server coroutines used here are usually: + - game scheduling/unscheduling + - game saving + - server saving + - notifications sending +""" +#pylint:disable=too-many-lines +import logging + +from tornado import gen +from tornado.concurrent import Future + +from diplomacy.communication import notifications, requests, responses +from diplomacy.server.notifier import Notifier +from diplomacy.server.server_game import ServerGame +from diplomacy.server.request_manager_utils import (SynchronizedData, verify_request, transfer_special_tokens, + assert_game_not_finished) +from diplomacy.utils import exceptions, strings, constants, export +from diplomacy.utils.common import hash_password +from diplomacy.utils.constants import OrderSettings +from diplomacy.utils.game_phase_data import GamePhaseData + +LOGGER = logging.getLogger(__name__) + +# ================= +# Request managers. +# ================= + +def on_clear_centers(server, request, connection_handler): + """ Manage request ClearCenters. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.ClearCenters + """ + level = verify_request(server, request, connection_handler, observer_role=False) + assert_game_not_finished(level.game) + level.game.clear_centers(level.power_name) + Notifier(server, ignore_addresses=[request.address_in_game]).notify_cleared_centers(level.game, level.power_name) + +def on_clear_orders(server, request, connection_handler): + """ Manage request ClearOrders. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.ClearOrders + """ + level = verify_request(server, request, connection_handler, observer_role=False) + assert_game_not_finished(level.game) + level.game.clear_orders(level.power_name) + Notifier(server, ignore_addresses=[request.address_in_game]).notify_cleared_orders(level.game, level.power_name) + +def on_clear_units(server, request, connection_handler): + """ Manage request ClearUnits. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.ClearUnits + """ + level = verify_request(server, request, connection_handler, observer_role=False) + assert_game_not_finished(level.game) + level.game.clear_units(level.power_name) + Notifier(server, ignore_addresses=[request.address_in_game]).notify_cleared_units(level.game, level.power_name) + +def on_create_game(server, request, connection_handler): + """ Manage request CreateGame. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.CreateGame + """ + + # Check request token. + verify_request(server, request, connection_handler) + game_id, token, power_name, state = request.game_id, request.token, request.power_name, request.state + + # Check if server still accepts to create new games. + if server.cannot_create_more_games(): + raise exceptions.GameCreationException() + + # Check if given map name is valid and if there is such map. + game_map = server.get_map(request.map_name) + if not game_map: + raise exceptions.MapIdException() + + # If rule SOLITAIRE is required, a power name cannot be queried (as all powers should be dummy). + # In such case, game creator can only be omniscient. + if request.rules and 'SOLITAIRE' in request.rules and power_name is not None: + raise exceptions.GameSolitaireException() + + # If a power name is given, check if it's a valid power name for related map. + if power_name is not None and power_name not in game_map['powers']: + raise exceptions.MapPowerException(power_name) + + # Create server game. + username = server.users.get_name(token) + if game_id is None or game_id == '': + game_id = server.create_game_id() + elif server.has_game_id(game_id): + raise exceptions.GameIdException('Game ID already used (%s).' % game_id) + server_game = ServerGame(map_name=request.map_name, + rules=request.rules, + game_id=game_id, + initial_state=state, + n_controls=request.n_controls, + deadline=request.deadline, + registration_password=request.registration_password) + server_game.server = server + + # Make sure game creator will be a game master (set him as moderator if he's not an admin). + if not server.users.has_admin(username): + server_game.promote_moderator(username) + + # Register game creator, as either power player or omniscient observer. + if power_name: + server_game.control(power_name, username, token) + client_game = server_game.as_power_game(power_name) + else: + server_game.add_omniscient_token(token) + client_game = server_game.as_omniscient_game(username) + + # Register game on server. + server.add_new_game(server_game) + + # Start game immediately if possible (e.g. if it's a solitaire game). + if server_game.game_can_start(): + server.start_game(server_game) + + server.save_game(server_game) + + return responses.DataGame(data=client_game, request_id=request.request_id) + +def on_delete_account(server, request, connection_handler): + """ Manage request DeleteAccount. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.DeleteAccount + """ + + # Check request token. + verify_request(server, request, connection_handler) + token, username = request.token, request.username + + # Get username of account to delete, either from given username or from request token. + # If given username is not token username, admin privileges are required to delete account of given username. + if not username: + username = server.users.get_name(token) + elif username != server.users.get_name(token): + server.assert_admin_token(token) + + # Delete account. + if server.users.has_username(username): + + # Send notification about account deleted to all account tokens. + Notifier(server, ignore_tokens=[token]).notify_account_deleted(username) + + # Delete user from server. + server.users.remove_user(username) + + # Remove tokens related to this account from loaded server games. + # Unregister this account from moderators, omniscient observers and players of loaded games. + for server_game in server.games.values(): # type: ServerGame + server_game.filter_tokens(server.users.has_token) + filter_status = server_game.filter_usernames(server.users.has_username) + + # If this account was a player for this game, notify game about new dummy powers. + if filter_status > 0: + server.stop_game_if_needed(server_game) + Notifier(server, ignore_tokens=[token]).notify_game_powers_controllers(server_game) + + # Require game disk backup. + server.save_game(server_game) + + # Require server data disk backup. + server.save_data() + +def on_delete_game(server, request, connection_handler): + """ Manage request DeleteGame. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.DeleteGame + """ + level = verify_request(server, request, connection_handler, observer_role=False, power_role=False) + server.delete_game(level.game) + server.unschedule_game(level.game) + Notifier(server, ignore_tokens=[request.token]).notify_game_deleted(level.game) + +def on_get_dummy_waiting_powers(server, request, connection_handler): + """ Manage request GetAllDummyPowerNames. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: an instance of responses.DataGamesToPowerNames + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.GetDummyWaitingPowers + """ + verify_request(server, request, connection_handler) + return responses.DataGamesToPowerNames( + data=server.get_dummy_waiting_power_names(request.buffer_size, request.token), request_id=request.request_id) + +def on_get_all_possible_orders(server, request, connection_handler): + """ Manage request GetAllPossibleOrders + :param server: server which receives the request + :param request: request to manage + :param connection_handler: connection handler from which the request was sent + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.GetAllPossibleOrders + """ + level = verify_request(server, request, connection_handler, require_master=False) + return responses.DataPossibleOrders(possible_orders=level.game.get_all_possible_orders(), + orderable_locations=level.game.get_orderable_locations(), + request_id=request.request_id) + +def on_get_available_maps(server, request, connection_handler): + """ Manage request GetAvailableMaps. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.GetAvailableMaps + """ + verify_request(server, request, connection_handler) + return responses.DataMaps(data=server.available_maps, request_id=request.request_id) + +def on_get_playable_powers(server, request, connection_handler): + """ Manage request GetPlayablePowers. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.GetPlayablePowers + """ + verify_request(server, request, connection_handler) + return responses.DataPowerNames( + data=server.get_game(request.game_id).get_dummy_power_names(), request_id=request.request_id) + +def on_get_phase_history(server, request, connection_handler): + """ Manage request GetPhaseHistory. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: a DataGamePhases object. + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.GetPhaseHistory + :rtype: diplomacy.communication.responses.DataGamePhases + """ + level = verify_request(server, request, connection_handler, require_master=False) + game_phases = level.game.get_phase_history(request.from_phase, request.to_phase, request.game_role) + return responses.DataGamePhases(data=game_phases, request_id=request.request_id) + +def on_join_game(server, request, connection_handler): + """ Manage request JoinGame. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: a Data response with client game data. + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.JoinGame + """ + + # Check request token. + verify_request(server, request, connection_handler) + token, power_name, registration_password = request.token, request.power_name, request.registration_password + + # Get related game. + server_game = server.get_game(request.game_id) # type: ServerGame + + username = server.users.get_name(token) + + # No power name given, request sender wants to be an observer. + if power_name is None: + + # Check given registration password for related game. + if not server_game.is_valid_password(registration_password) and not server.token_is_master(token, server_game): + raise exceptions.GameRegistrationPasswordException() + + # Request token must not already be a player token. + if server_game.has_player_token(token): + raise exceptions.GameJoinRoleException() + + # Observations must be allowed for this game, or request sender must be a game master. + if server_game.no_observations and not server.token_is_master(token, server_game): + raise exceptions.GameObserverException('Disallowed observation for non-master users.') + + # Flag used to check if token was already registered with expected game role + # (possibly because of a re-sent request). If True, we can send response + # immediately without saving anything. + token_already_registered = True + + if server.user_is_omniscient(username, server_game): + + # Request sender is allowed to be omniscient for this game. + # Let's set him as an omniscient observer. + + if not server_game.has_omniscient_token(token): + # Register request token as omniscient token. + server_game.add_omniscient_token(token) + token_already_registered = False + elif not request.re_sent: + # Token already registered but request is a new one. + # This should not happen (programming error?). + raise exceptions.ResponseException('Token already omniscient from a new request.') + + # Create client game. + client_game = server_game.as_omniscient_game(username) + + else: + + # Request sender is not allowed to be omniscient for this game. + # Let's set him as an observer. + + # A token should not be registered twice as observer token. + if not server_game.has_observer_token(token): + # Register request token as observer token. + server_game.add_observer_token(token) + token_already_registered = False + elif not request.re_sent: + # Token already registered but request is a new one. + # This should not happen (programming error?). + raise exceptions.ResponseException('Token already observer.') + + # Create client game. + client_game = server_game.as_observer_game(username) + + # If token was already registered, return immediately (no need to save anything). + if token_already_registered: + return responses.DataGame(data=client_game, request_id=request.request_id) + + # Power name given, request sender wants to be a player. + else: + + # Check given registration password for related game. + if not (server_game.is_valid_password(registration_password) + or server.token_is_master(token, server_game) + or username == constants.PRIVATE_BOT_USERNAME): + raise exceptions.GameRegistrationPasswordException() + + # No new player allowed if game is ended. + if server_game.is_game_completed or server_game.is_game_canceled: + raise exceptions.GameFinishedException() + + if not server_game.has_power(power_name): + raise exceptions.MapPowerException(power_name) + + if username == constants.PRIVATE_BOT_USERNAME: + # Private bot is allowed to control any dummy power after game started + # (ie. after reached expected number of real players). + # A dummy power controlled by bot is still marked as "dummy", but + # has tokens associated. + if not server_game.is_game_active and not server_game.is_game_paused: + raise exceptions.ResponseException('Game is not active.') + if power_name not in server_game.get_dummy_power_names(): + raise exceptions.ResponseException('Invalid dummy power name %s' % power_name) + server_game.get_power(power_name).add_token(token) + client_game = server_game.as_power_game(power_name) + return responses.DataGame(data=client_game, request_id=request.request_id) + + # Power already controlled by request sender. + if server_game.is_controlled_by(power_name, username): + + # Create client game. + client_game = server_game.as_power_game(power_name) + + # If token is already registered (probably because of a re-sent request), + # then we can send response immediately without saving anything. + if server_game.power_has_token(power_name, token): + return responses.DataGame(data=client_game, request_id=request.request_id) + + # Otherwise, register token. + server_game.get_power(power_name).add_token(token) + + # Power not already controlled by request sender. + else: + + # Request token must not be already an observer token or an omniscient token. + if server_game.has_observer_token(token) or server_game.has_omniscient_token(token): + raise exceptions.GameJoinRoleException() + + # If allowed number of players is already reached, only game masters are allowed to control dummy powers. + if server_game.has_expected_controls_count() and not server.token_is_master(token, server_game): + raise exceptions.ResponseException( + 'Reached maximum number of allowed controlled powers for this game (%d).' + % server_game.get_expected_controls_count()) + + # If power is already controlled (by someone else), game must allow to select a power randomly. + if server_game.is_controlled(power_name) and server_game.power_choice: + raise exceptions.ResponseException('You want to control a power that is already controlled,' + 'and this game does not allocate powers randomly.') + + # If request sender is already a game player and game does not allow multiple powers per player, + # then it cannot register. + if server_game.has_player(username) and not server_game.multiple_powers_per_player: + raise exceptions.ResponseException('Disallowed multiple powers per player.') + + # If game has no rule POWER_CHOICE, a randomly selected power is assigned to request sender, + # whatever be the power he queried. + if not server_game.power_choice: + power_name = server_game.get_random_power_name() + + # Register sender token as power token. + server_game.control(power_name, username, token) + + # Notify other game tokens about new powers controllers. + Notifier(server, ignore_addresses=[(power_name, token)]).notify_game_powers_controllers(server_game) + + # Create client game. + client_game = server_game.as_power_game(power_name) + + # Start game if it can start. + if server_game.game_can_start(): + server.start_game(server_game) + + # Require game disk backup. + server.save_game(server_game) + + return responses.DataGame(data=client_game, request_id=request.request_id) + +def on_join_powers(server, request, connection_handler): + """ Manage request JoinPowers. + Current code does not care about rule POWER_CHOICE. It only + checks if queried powers can be joined by request sender. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None. + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.JoinPowers + """ + + # Check request token. + verify_request(server, request, connection_handler) + token, power_names = request.token, request.power_names + username = server.users.get_name(token) + + if not power_names: + raise exceptions.ResponseException('Required at least 1 power name to join powers.') + + # Get related game. + server_game = server.get_game(request.game_id) # type: ServerGame + + # No new player allowed if game is ended. + if server_game.is_game_completed or server_game.is_game_canceled: + raise exceptions.GameFinishedException() + + # Check given registration password for related game. + if not (server_game.is_valid_password(request.registration_password) + or server.token_is_master(token, server_game) + or username == constants.PRIVATE_BOT_USERNAME): + raise exceptions.GameRegistrationPasswordException() + + # Check if given power names are valid. + for power_name in power_names: + if not server_game.has_power(power_name): + raise exceptions.MapPowerException(power_name) + + dummy_power_names = server_game.get_dummy_power_names() + + if username == constants.PRIVATE_BOT_USERNAME: + # Private bot is allowed to control any dummy power after game started + # (ie. after reached expected number of real players). + # A dummy power controlled by bot is still marked as "dummy", but + # has tokens associated. + + # Check if game is started. + if server_game.is_game_forming: + raise exceptions.ResponseException('Game is not active.') + + # Check if all given power names are dummy. + for power_name in power_names: + if power_name not in dummy_power_names: + raise exceptions.ResponseException('Invalid dummy power name %s' % power_name) + + # Join bot to each given power name. + for power_name in power_names: + server_game.get_power(power_name).add_token(token) + + # Done with bot. + server.save_game(server_game) + return + + # Request token must not be already an observer token or an omniscient token. + if server_game.has_observer_token(token) or server_game.has_omniscient_token(token): + raise exceptions.GameJoinRoleException() + + # All given powers must be dummy or already controlled by request sender. + required_dummy_powers = set() + for power_name in power_names: + power = server_game.get_power(power_name) + if power.is_dummy(): + required_dummy_powers.add(power_name) + elif not power.is_controlled_by(username): + raise exceptions.ResponseException('Power %s is controlled by someone else.' % power_name) + + # Nothing to do if all queried powers are already controlled by request sender. + if not required_dummy_powers: + server.save_game(server_game) + return + + # Do additional checks for non-game masters. + if not server.token_is_master(token, server_game): + + if len(required_dummy_powers) < len(power_names) and not server_game.multiple_powers_per_player: + # Request sender already controls some powers but game does not allow multiple powers per player. + raise exceptions.ResponseException('Disallowed multiple powers per player.') + + if server_game.has_expected_controls_count(): + # Allowed number of players is already reached for this game. + raise exceptions.ResponseException( + 'Reached maximum number of allowed controlled powers for this game (%d).' + % server_game.get_expected_controls_count()) + + # Join user to each queried dummy power. + for power_name in required_dummy_powers: + server_game.control(power_name, username, token) + + # Notify game about new powers controllers. + + Notifier(server).notify_game_powers_controllers(server_game) + + # Start game if it can start. + if server_game.game_can_start(): + server.start_game(server_game) + + # Require game disk backup. + server.save_game(server_game) + +def on_leave_game(server, request, connection_handler): + """ Manage request LeaveGame. + If user is an (omniscient) observer, stop observation. + Else, stop to control given power name. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.LeaveGame + """ + level = verify_request(server, request, connection_handler, require_master=False) + if level.is_power(): + level.game.set_controlled(level.power_name, None) + Notifier(server, ignore_addresses=[request.address_in_game]).notify_game_powers_controllers(level.game) + server.stop_game_if_needed(level.game) + else: + level.game.remove_special_token(request.game_role, request.token) + server.save_game(level.game) + +def on_list_games(server, request, connection_handler): + """ Manage request ListGames. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: an instance of responses.DataGames + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.ListGames + """ + verify_request(server, request, connection_handler) + if request.map_name is not None and server.get_map(request.map_name) is None: + raise exceptions.MapIdException() + selected_game_indices = [] + for game_id in server.get_game_indices(): + if request.game_id and request.game_id not in game_id: + continue + server_game = server.load_game(game_id) + if request.for_omniscience and not server.token_is_omniscient(request.token, server_game): + continue + if not request.include_protected and server_game.registration_password is not None: + continue + if request.status and server_game.status != request.status: + continue + if request.map_name and server_game.map_name != request.map_name: + continue + username = server.users.get_name(request.token) + selected_game_indices.append(responses.DataGameInfo( + game_id=server_game.game_id, + phase=server_game.current_short_phase, + timestamp=server_game.get_latest_timestamp(), + map_name=server_game.map_name, + observer_level=server_game.get_observer_level(username), + controlled_powers=server_game.get_controlled_power_names(username), + rules=server_game.rules, + status=server_game.status, + n_players=server_game.count_controlled_powers(), + n_controls=server_game.get_expected_controls_count(), + deadline=server_game.deadline, + registration_password=bool(server_game.registration_password) + )) + return responses.DataGames(data=selected_game_indices, request_id=request.request_id) + +def on_get_games_info(server, request, connection_handler): + """ Manage request GetGamesInfo. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: an instance of responses.DataGames + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.GetGamesInfo + """ + verify_request(server, request, connection_handler) + username = server.users.get_name(request.token) + games = [] + for game_id in request.games: + try: + server_game = server.load_game(game_id) + games.append(responses.DataGameInfo( + game_id=server_game.game_id, + phase=server_game.current_short_phase, + timestamp=server_game.get_latest_timestamp(), + map_name=server_game.map_name, + observer_level=server_game.get_observer_level(username), + controlled_powers=server_game.get_controlled_power_names(username), + rules=server_game.rules, + status=server_game.status, + n_players=server_game.count_controlled_powers(), + n_controls=server_game.get_expected_controls_count(), + deadline=server_game.deadline, + registration_password=bool(server_game.registration_password) + )) + except exceptions.GameIdException: + # Invalid game ID, just pass. + pass + return responses.DataGames(data=games, request_id=request.request_id) + +def on_logout(server, request, connection_handler): + """ Manage request Logout. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.Logout + """ + verify_request(server, request, connection_handler) + server.remove_token(request.token) + +def on_process_game(server, request, connection_handler): + """ Manage request ProcessGame. + Force a game to be processed the sooner. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.ProcessGame + """ + level = verify_request(server, request, connection_handler, observer_role=False, power_role=False) + assert_game_not_finished(level.game) + for power_name in level.game.get_map_power_names(): + # Force power to not wait and tag it as if it has orders. + # (this is valid only for this processing and will be reset for next phase). + power = level.game.get_power(power_name) + power.order_is_set = OrderSettings.ORDER_SET + power.wait = False + if level.game.status == strings.FORMING: + level.game.set_status(strings.ACTIVE) + server.force_game_processing(level.game) + +@gen.coroutine +def on_query_schedule(server, request, connection_handler): + """ Manage request QuerySchedule. + Force a game to be processed the sooner. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.QuerySchedule + """ + level = verify_request(server, request, connection_handler, require_master=False) + schedule_event = yield server.games_scheduler.get_info(level.game) + if not schedule_event: + raise exceptions.ResponseException('Game not scheduled.') + return responses.DataGameSchedule( + game_id=level.game.game_id, + phase=level.game.current_short_phase, + schedule=schedule_event, + request_id=request.request_id + ) + +def on_save_game(server, request, connection_handler): + """ Manage request SaveGame + :param server: server which receives the request + :param request: request to manage + :param connection_handler: connection handler from which the request was sent + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.SaveGame + """ + level = verify_request(server, request, connection_handler, require_master=False) + game_json = export.to_saved_game_format(level.game) + return responses.DataSavedGame(data=game_json, request_id=request.request_id) + +def on_send_game_message(server, request, connection_handler): + """ Manage request SendGameMessage. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.SendGameMessage + """ + level = verify_request(server, request, connection_handler, omniscient_role=False, observer_role=False) + token, message = request.token, request.message + assert_game_not_finished(level.game) + if level.game.no_press: + raise exceptions.ResponseException('Messages not allowed for this game.') + if request.game_role != message.sender: + raise exceptions.ResponseException('A power can only send its own messages.') + + if not level.game.has_power(message.sender): + raise exceptions.MapPowerException(message.sender) + if not request.message.is_global(): + if level.game.public_press: + raise exceptions.ResponseException('Only public messages allowed for this game.') + if not level.game.is_game_active: + raise exceptions.GameNotPlayingException() + if level.game.current_short_phase != message.phase: + raise exceptions.GamePhaseException(level.game.current_short_phase, message.phase) + if not level.game.has_power(message.recipient): + raise exceptions.MapPowerException(message.recipient) + username = server.users.get_name(token) + power_name = message.sender + if not level.game.is_controlled_by(power_name, username): + raise exceptions.ResponseException('Power name %s is not controlled by given username.' % power_name) + if message.sender == message.recipient: + raise exceptions.ResponseException('A power cannot send message to itself.') + + if request.re_sent: + # Request is re-sent (e.g. after a synchronization). We may have already received this message. + # lookup message. WARNING: This may take time if there are many messages. How to improve that ? + for archived_message in level.game.messages.reversed_values(): + if (archived_message.sender == message.sender + and archived_message.recipient == message.recipient + and archived_message.phase == message.phase + and archived_message.message == message.message): + # Message found. Send archived time_sent, don't notify anyone. + LOGGER.warning('Game message re-sent.') + return responses.DataTimeStamp(data=archived_message.time_sent, request_id=request.request_id) + # If message not found, consider it as a new message. + if message.time_sent is not None: + raise exceptions.ResponseException('Server cannot receive a message with a time sent already set.') + message.time_sent = level.game.add_message(message) + Notifier(server, ignore_addresses=[(request.game_role, token)]).notify_game_message(level.game, message) + server.save_game(level.game) + return responses.DataTimeStamp(data=message.time_sent, request_id=request.request_id) + +def on_set_dummy_powers(server, request, connection_handler): + """ Manage request SetDummyPowers. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.SetDummyPowers + """ + level = verify_request(server, request, connection_handler, observer_role=False, power_role=False) + assert_game_not_finished(level.game) + username, power_names = request.username, request.power_names + if username is not None and not server.users.has_username(username): + raise exceptions.UserException() + if power_names: + power_names = [power_name for power_name in power_names if level.game.has_power(power_name)] + else: + power_names = list(level.game.get_map_power_names()) + if username is not None: + power_names = [power_name for power_name in power_names + if level.game.is_controlled_by(power_name, username)] + count_before = level.game.count_controlled_powers() + level.game.update_dummy_powers(power_names) + if count_before != level.game.count_controlled_powers(): + server.stop_game_if_needed(level.game) + Notifier(server).notify_game_powers_controllers(level.game) + server.save_game(level.game) + +def on_set_game_state(server, request, connection_handler): + """ Manage request SetGameState. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.SetGameState + """ + level = verify_request(server, request, connection_handler, observer_role=False, power_role=False) + level.game.set_phase_data(GamePhaseData( + request.phase, request.state, request.orders, request.results, request.messages)) + server.stop_game_if_needed(level.game) + Notifier(server, ignore_addresses=[request.address_in_game]).notify_game_phase_data(level.game) + server.save_game(level.game) + +def on_set_game_status(server, request, connection_handler): + """ Manage request SetGameStatus. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.SetGameStatus + """ + level = verify_request(server, request, connection_handler, observer_role=False, power_role=False) + status = request.status + previous_status = level.game.status + if previous_status != status: + if previous_status == strings.CANCELED: + raise exceptions.GameCanceledException() + if previous_status == strings.COMPLETED: + raise exceptions.GameFinishedException() + level.game.set_status(status) + if status == strings.COMPLETED: + phase_data_before_draw, phase_data_after_draw = level.game.draw() + server.unschedule_game(level.game) + Notifier(server).notify_game_processed(level.game, phase_data_before_draw, phase_data_after_draw) + else: + if status == strings.ACTIVE: + server.schedule_game(level.game) + elif status == strings.PAUSED: + server.unschedule_game(level.game) + elif status == strings.CANCELED: + server.unschedule_game(level.game) + if server.remove_canceled_games: + server.delete_game(level.game) + Notifier(server, ignore_addresses=[request.address_in_game]).notify_game_status(level.game) + server.save_game(level.game) + +def on_set_grade(server, request, connection_handler): + """ Manage request SetGrade. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.SetGrade + """ + + # Check request token. + verify_request(server, request, connection_handler) + token, grade, grade_update, username, game_id = ( + request.token, request.grade, request.grade_update, request.username, request.game_id) + + to_save = False + + if grade == strings.ADMIN: + + # Requested admin grade update. + + # Check if request token is admin. + server.assert_admin_token(token) + + # Promote username to administrator only if not already admin. + # Demote username from administration only if already admin. + if grade_update == strings.PROMOTE: + if not server.users.has_admin(username): + server.users.add_admin(username) + to_save = True + elif server.users.has_admin(username): + server.users.remove_admin(username) + to_save = True + + if to_save: + + # Require server data disk backup. + server.save_data() + + # Update each loaded games where user was connected as observer or omniscient + # without explicitly allowed to be moderator or omniscient. This means its + # observer role has changed (observer -> omniscient or vice versa) in related games. + for server_game in server.games.values(): # type: ServerGame + + # We check games where user is not explicitly allowed to be moderator or omniscient. + if not server_game.is_moderator(username) and not server_game.is_omniscient(username): + transfer_special_tokens(server_game, server, username, grade_update, + grade_update == strings.PROMOTE) + + else: + # Requested omniscient or moderator grade update for a specific game. + + # Get related game. + server_game = server.get_game(game_id) + + # Check if request sender is a game master. + server.assert_master_token(token, server_game) + + # We must check if grade update changes omniscient rights for user. + # Reminder: a user is omniscient if either server admin, game moderator or game explicit omniscient. + # So, even if moderator or explicit omniscient grade is updated for user, his omniscient rights + # may not change. + user_is_omniscient_before = server.user_is_omniscient(username, server_game) + + if grade == strings.OMNISCIENT: + + # Promote explicitly user to omniscient only if not already explicit omniscient. + # Demote explicitly user from omniscience only if already explicit omniscient. + + if grade_update == strings.PROMOTE: + if not server_game.is_omniscient(username): + server_game.promote_omniscient(username) + to_save = True + elif server_game.is_omniscient(username): + server_game.demote_omniscient(username) + to_save = True + else: + + # Promote user to moderator if not already moderator. + # Demote user from moderation if already moderator. + + if grade_update == strings.PROMOTE: + if not server_game.is_moderator(username): + server_game.promote_moderator(username) + to_save = True + elif server_game.is_moderator(username): + server_game.demote_moderator(username) + to_save = True + + if to_save: + + # Require game disk backup. + server.save_game(server_game) + + # Check if user omniscient rights was changed. + user_is_omniscient_after = server.user_is_omniscient(username, server_game) + if user_is_omniscient_before != user_is_omniscient_after: + + transfer_special_tokens(server_game, server, username, grade_update, user_is_omniscient_after) + +def on_set_orders(server, request, connection_handler): + """ Manage request SetOrders. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.SetOrders + """ + level = verify_request(server, request, connection_handler, observer_role=False, require_power=True) + assert_game_not_finished(level.game) + power = level.game.get_power(level.power_name) + previous_wait = power.wait + power.clear_orders() + power.wait = previous_wait + level.game.set_orders(level.power_name, request.orders) + # Notify other power tokens. + Notifier(server, ignore_addresses=[request.address_in_game]).notify_power_orders_update( + level.game, level.game.get_power(level.power_name), request.orders) + if request.wait is not None: + level.game.set_wait(level.power_name, request.wait) + Notifier(server, ignore_addresses=[request.address_in_game]).notify_power_wait_flag( + level.game, level.game.get_power(level.power_name), request.wait) + if level.game.does_not_wait(): + server.force_game_processing(level.game) + server.save_game(level.game) + +def on_set_wait_flag(server, request, connection_handler): + """ Manage request SetWaitFlag. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.SetWaitFlag + """ + level = verify_request(server, request, connection_handler, observer_role=False, require_power=True) + assert_game_not_finished(level.game) + level.game.set_wait(level.power_name, request.wait) + # Notify other power tokens. + Notifier(server, ignore_addresses=[request.address_in_game]).notify_power_wait_flag( + level.game, level.game.get_power(level.power_name), request.wait) + if level.game.does_not_wait(): + server.force_game_processing(level.game) + server.save_game(level.game) + +def on_sign_in(server, request, connection_handler): + """ Manage request SignIn. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.SignIn + """ + # No channel/game request verification to do. + username, password, create_user = request.username, request.password, request.create_user + if create_user: + # Register. + if not username: + raise exceptions.UserException() + if not password: + raise exceptions.PasswordException() + if not server.allow_registrations: + raise exceptions.ServerRegistrationException() + if server.users.has_username(username): + raise exceptions.UserException() + server.users.add_user(username, hash_password(password)) + elif not server.users.has_user(username, password): + raise exceptions.UserException() + token = server.users.connect_user(username, connection_handler) + server.save_data() + return responses.DataToken(data=token, request_id=request.request_id) + +def on_synchronize(server, request, connection_handler): + """ Manage request Synchronize. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.Synchronize + """ + + level = verify_request(server, request, connection_handler, require_master=False) + + # Get sync data. + + timestamp = request.timestamp + if request.game_role == strings.OBSERVER_TYPE: + assert level.game.has_observer_token(request.token) + elif request.game_role == strings.OMNISCIENT_TYPE: + assert level.game.has_omniscient_token(request.token) + elif not level.game.power_has_token(request.game_role, request.token): + raise exceptions.GamePlayerException() + messages = level.game.get_messages(request.game_role, timestamp + 1) + if level.is_power(): + # Don't notify a power about messages she sent herself. + messages = {message.time_sent: message for message in messages.values() + if message.sender != level.power_name} + phase_data_list = level.game.phase_history_from_timestamp(timestamp + 1) + current_phase_data = None + if phase_data_list: + # If there is no new state history, then current state should have not changed + # and does not need to be sent. Otherwise current state is a new state + # got after a processing, and must be sent. + current_phase_data = level.game.get_phase_data() + data_to_send = [SynchronizedData(message.time_sent, 0, 'message', message) for message in messages.values()] + data_to_send += [SynchronizedData(phase_data.state['timestamp'], 1, 'state_history', phase_data) + for phase_data in phase_data_list] + if current_phase_data: + data_to_send.append(SynchronizedData(current_phase_data.state['timestamp'], 2, 'phase', current_phase_data)) + data_to_send.sort(key=lambda x: (x.timestamp, x.order)) + + # Send sync data. + + notifier = Notifier(server) + if strings.role_is_special(request.game_role): + addresses = [request.address_in_game] + else: + addresses = list(level.game.get_power_addresses(request.game_role)) + + for data in data_to_send: + if data.type == 'message': + notifier.notify_game_addresses( + level.game.game_id, addresses, notifications.GameMessageReceived, message=data.data) + else: + if data.type not in ('state_history', 'phase'): + raise AssertionError('Unknown synchronized data.') + phase_data = level.game.filter_phase_data(data.data, request.game_role, is_current=(data.type == 'phase')) + notifier.notify_game_addresses(level.game.game_id, addresses, notifications.GamePhaseUpdate, + phase_data=phase_data, phase_data_type=data.type) + # Send game status. + notifier.notify_game_addresses(level.game.game_id, addresses, notifications.GameStatusUpdate, + status=level.game.status) + return responses.DataGameInfo(game_id=level.game.game_id, + phase=level.game.current_short_phase, + timestamp=level.game.get_latest_timestamp(), + request_id=request.request_id) + +def on_vote(server, request, connection_handler): + """ Manage request Vote. + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.Vote + """ + level = verify_request(server, request, connection_handler, + omniscient_role=False, observer_role=False, require_power=True) + assert_game_not_finished(level.game) + power = level.game.get_power(level.power_name) + if power.is_eliminated(): + raise exceptions.ResponseException('Power %s is eliminated.' % power.name) + if not power.is_controlled_by(server.users.get_name(request.token)): + raise exceptions.GamePlayerException() + power.vote = request.vote + Notifier(server).notify_game_vote_updated(level.game) + if level.game.has_draw_vote(): + # Votes allows to draw the game. + phase_data_before_draw, phase_data_after_draw = level.game.draw() + server.unschedule_game(level.game) + Notifier(server).notify_game_processed(level.game, phase_data_before_draw, phase_data_after_draw) + server.save_game(level.game) + + +# Mapping dictionary from request class to request handler function. +MAPPING = { + requests.ClearCenters: on_clear_centers, + requests.ClearOrders: on_clear_orders, + requests.ClearUnits: on_clear_units, + requests.CreateGame: on_create_game, + requests.DeleteAccount: on_delete_account, + requests.DeleteGame: on_delete_game, + requests.GetDummyWaitingPowers: on_get_dummy_waiting_powers, + requests.GetAllPossibleOrders: on_get_all_possible_orders, + requests.GetAvailableMaps: on_get_available_maps, + requests.GetPlayablePowers: on_get_playable_powers, + requests.GetPhaseHistory: on_get_phase_history, + requests.JoinGame: on_join_game, + requests.JoinPowers: on_join_powers, + requests.LeaveGame: on_leave_game, + requests.ListGames: on_list_games, + requests.GetGamesInfo: on_get_games_info, + requests.Logout: on_logout, + requests.ProcessGame: on_process_game, + requests.QuerySchedule: on_query_schedule, + requests.SaveGame: on_save_game, + requests.SendGameMessage: on_send_game_message, + requests.SetDummyPowers: on_set_dummy_powers, + requests.SetGameState: on_set_game_state, + requests.SetGameStatus: on_set_game_status, + requests.SetGrade: on_set_grade, + requests.SetOrders: on_set_orders, + requests.SetWaitFlag: on_set_wait_flag, + requests.SignIn: on_sign_in, + requests.Synchronize: on_synchronize, + requests.Vote: on_vote, +} + +def handle_request(server, request, connection_handler): + """ (coroutine) Find request handler function for associated request, run it and return its result. + :param server: a Server object to pass to handler function. + :param request: a request object to pass to handler function. + See diplomacy.communication.requests for possible requests. + :param connection_handler: a ConnectionHandler object to pass to handler function. + :return: (future) either None or a response object. + See module diplomacy.communication.responses for possible responses. + """ + request_handler_fn = MAPPING.get(type(request), None) + if not request_handler_fn: + raise exceptions.RequestException() + if gen.is_coroutine_function(request_handler_fn): + # Throw the future returned by this coroutine. + return request_handler_fn(server, request, connection_handler) + # Create and return a future. + future = Future() + try: + result = request_handler_fn(server, request, connection_handler) + future.set_result(result) + except exceptions.DiplomacyException as exc: + future.set_exception(exc) + return future diff --git a/diplomacy/server/run.py b/diplomacy/server/run.py new file mode 100755 index 0000000..f47ed4f --- /dev/null +++ b/diplomacy/server/run.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# ============================================================================== +# 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/>. +# ============================================================================== +""" Small module script to quickly start a server with pretty log-printing. + You can stop the server with keyboard interruption (Ctrl+C). Usage: + python -m diplomacy.server.run # run on port 8432. + python -m diplomacy.server.run --port=<given port> # run on given port. +""" +import argparse +from diplomacy import Server +from diplomacy.utils import constants + +PARSER = argparse.ArgumentParser(description='Run server.') +PARSER.add_argument('--port', '-p', type=int, default=constants.DEFAULT_PORT, + help='run on the given port (default: %s)' % constants.DEFAULT_PORT) +ARGS = PARSER.parse_args() + +try: + Server().start(port=ARGS.port) +except KeyboardInterrupt: + print('Keyboard interruption.') diff --git a/diplomacy/server/scheduler.py b/diplomacy/server/scheduler.py new file mode 100644 index 0000000..28bee74 --- /dev/null +++ b/diplomacy/server/scheduler.py @@ -0,0 +1,265 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Scheduler used by server to run games. + + Scheduler is configured with a task manager (callback function) and a step time (in seconds) + which indicates how long it must wait at each step before checking tasks to process. + Then, to add a task, user must specify a data to process and a delay (in number of step times). + Deadline is computed using given delay + scheduler step when data was added. + + To set unit as a minute, create Scheduler with unit_in_seconds = 60. + In such case, a task with deadline 2 means 2 minutes to wait to process this task. + TO set unit as a second, create Scheduler with unit_in_seconds = 1. + In such case, a task with deadline 2 means 2 seconds to wait to process this task. +""" +from tornado import gen +from tornado.locks import Lock +from tornado.queues import Queue + +from diplomacy.utils.scheduler_event import SchedulerEvent +from diplomacy.utils import exceptions +from diplomacy.utils.priority_dict import PriorityDict + +class _Deadline(): + """ (internal) Deadline value, defined by a start time and a delay, such that deadline = start time + delay. """ + __slots__ = ['start_time', 'delay'] + + def __init__(self, start_time, delay): + """ Initialize a deadline with start time and delay, so that deadline = start time + delay. + :param start_time: (int) + :param delay: (int) + """ + self.start_time = start_time + self.delay = delay + + @property + def deadline(self): + """ Compute and return deadline. """ + return self.start_time + self.delay + + def __str__(self): + return 'Deadline(%d + %d = %d)' % (self.start_time, self.delay, self.deadline) + + def __lt__(self, other): + return self.deadline < other.deadline + +class _Task(): + """ (internal) Task class used by scheduler to order scheduled data. It allows auto-rescheduling + of a task after it was processed, until either: + - task delay is 0. + - task manager return a True boolean value (means "data fully processed"). + - scheduler is explicitly required to remove associated data. + """ + __slots__ = ['data', 'deadline', 'valid'] + + def __init__(self, data, deadline): + """ Initialize a task. + :param data: data to process. + :param deadline: Deadline object. + :type deadline: _Deadline + """ + self.data = data + self.deadline = deadline + self.valid = True # Used to ease task removal from Tornado queue. + + def __str__(self): + return '%s(%s, %s)' % (self.__class__.__name__, type(self.data).__name__, self.deadline) + + def update_delay(self, new_delay): + """ Change deadline delay with given new delay. """ + self.deadline.delay = new_delay + +class _ImmediateTask(_Task): + """ (internal) Represents a task intended to be processed as soon as possible the first time, + and then scheduled as a normal task for next times. As deadline does not matter for first + processing, an immediate task needs a processing validator called the first + time to check if it must still be processed. Note that, if validation returns False, + the task is not processed the first time and not even added to scheduler for next times. + """ + __slots__ = ['validator'] + + def __init__(self, data, future_delay, processing_validator): + """ Initialize an immediate task. + :param data: data to process. + :param future_delay: delay to use to reschedule that task after first processing. + :param processing_validator: either a Bool or a callable receiving the data and + returning a Bool: processing_validator(data) -> Bool. + Validator is used only for the first processing. If evaluated to True, task is + processed and then rescheduled for next processing with given future delay. + If evaluated to False, task is drop (neither processed nor rescheduled). + """ + super(_ImmediateTask, self).__init__(data, _Deadline(-future_delay, future_delay)) + if isinstance(processing_validator, bool): + self.validator = lambda: processing_validator + elif callable(processing_validator): + self.validator = lambda: processing_validator(data) + else: + raise RuntimeError('Validator for immediate task must be either a boolean or a callback(data).') + + def can_still_process(self): + """ Return True if this immediate task can still be processed for the first time. + If False is returned, task is drop and never processed (not even for a first time). + """ + return self.validator() + + def update_delay(self, new_delay): + self.deadline.start_time = -new_delay + self.deadline.delay = new_delay + +class Scheduler(): + """ (public) Scheduler class. """ + __slots__ = ['unit', 'current_time', 'callback_process', 'data_in_queue', 'data_in_heap', 'tasks_queue', 'lock'] + + def __init__(self, unit_in_seconds, callback_process): + """ Initialize a scheduler. + :param unit_in_seconds: number of seconds to wait for each step. + :param callback_process: callback to call on every task. + Signature: + task_callback(task.data) -> bool + If callback return True, task is considered done and is removed from scheduler. + Otherwise, task is rescheduled for another delay. + """ + assert isinstance(unit_in_seconds, int) and unit_in_seconds > 0 + assert callable(callback_process) + self.unit = unit_in_seconds + self.current_time = 0 + self.callback_process = callback_process + self.data_in_heap = PriorityDict() # data => Deadline + self.data_in_queue = {} # type: dict{object, _Task} # data => associated Task in queue + self.tasks_queue = Queue() + # Lock to modify this object safely inside one Tornado thread: + # http://www.tornadoweb.org/en/stable/locks.html + self.lock = Lock() + + def _enqueue(self, task): + """ Put a task in queue of tasks to process now. """ + self.data_in_queue[task.data] = task + self.tasks_queue.put_nowait(task) + + @gen.coroutine + def has_data(self, data): + """ Return True if given data is associated to any task. """ + with (yield self.lock.acquire()): + return data in self.data_in_heap or data in self.data_in_queue + + @gen.coroutine + def get_info(self, data): + """ Return info about scheduling for given data, or None if data is not found. """ + with (yield self.lock.acquire()): + deadline = None # type: _Deadline + if data in self.data_in_heap: + deadline = self.data_in_heap[data] + if data in self.data_in_queue: + deadline = self.data_in_queue[data].deadline + if deadline: + return SchedulerEvent(time_unit=self.unit, + time_added=deadline.start_time, + delay=deadline.delay, + current_time=self.current_time) + return None + + @gen.coroutine + def add_data(self, data, nb_units_to_wait): + """ Add data with a non-null deadline. For null deadlines, use no_wait(). + :param data: data to add + :param nb_units_to_wait: time to wait (in number of units) + """ + if not isinstance(nb_units_to_wait, int) or nb_units_to_wait <= 0: + raise exceptions.NaturalIntegerNotNullException() + with (yield self.lock.acquire()): + if data in self.data_in_heap or data in self.data_in_queue: + raise exceptions.AlreadyScheduledException() + # Add task to scheduler. + self.data_in_heap[data] = _Deadline(self.current_time, nb_units_to_wait) + + @gen.coroutine + def no_wait(self, data, nb_units_to_wait, processing_validator): + """ Add a data to be processed the sooner. + :param data: data to add + :param nb_units_to_wait: time to wait (in number of units) for data tasks after first task is executed. + If null (0), data is processed once (first time) and then dropped. + :param processing_validator: validator used to check if data can still be processed for the first time. + See documentation of class _ImmediateTask for more details. + """ + if not isinstance(nb_units_to_wait, int) or nb_units_to_wait < 0: + raise exceptions.NaturalIntegerException() + with (yield self.lock.acquire()): + if data in self.data_in_heap: + # Move data from heap to queue with new delay. + del self.data_in_heap[data] + self._enqueue(_ImmediateTask(data, nb_units_to_wait, processing_validator)) + elif data in self.data_in_queue: + # Change delay for future scheduling. + self.data_in_queue[data].update_delay(nb_units_to_wait) + else: + # Add data to queue. + self._enqueue(_ImmediateTask(data, nb_units_to_wait, processing_validator)) + + @gen.coroutine + def remove_data(self, data): + """ Remove a data (and all associated tasks) from scheduler. """ + with (yield self.lock.acquire()): + if data in self.data_in_heap: + del self.data_in_heap[data] + elif data in self.data_in_queue: + # Remove task from data_in_queue and invalidate it in queue. + self.data_in_queue.pop(data).valid = False + + @gen.coroutine + def _step(self): + """ Compute a step (check and enqueue tasks to run now) in scheduler. """ + with (yield self.lock.acquire()): + self.current_time += 1 + while self.data_in_heap: + deadline, data = self.data_in_heap.smallest() + if deadline.deadline > self.current_time: + break + del self.data_in_heap[data] + self._enqueue(_Task(data, deadline)) + + @gen.coroutine + def schedule(self): + """ Main scheduler method (callback to register in ioloop). Wait for unit seconds and + run tasks after each wait time. + """ + while True: + yield gen.sleep(self.unit) + yield self._step() + + @gen.coroutine + def process_tasks(self): + """ Main task processing method (callback to register in ioloop). Consume and process tasks in queue + and reschedule processed tasks when relevant. + A task is processed if associated data was not removed from scheduler. + A task is rescheduler if processing callback returns False (True meaning `task definitively done`) + AND if task deadline is not null. + """ + while True: + task = yield self.tasks_queue.get() # type: _Task + try: + if task.valid and (not isinstance(task, _ImmediateTask) or task.can_still_process()): + if gen.is_coroutine_function(self.callback_process): + remove_data = yield self.callback_process(task.data) + else: + remove_data = self.callback_process(task.data) + remove_data = remove_data or not task.deadline.delay + with (yield self.lock.acquire()): + del self.data_in_queue[task.data] + if not remove_data: + self.data_in_heap[task.data] = _Deadline(self.current_time, task.deadline.delay) + finally: + self.tasks_queue.task_done() diff --git a/diplomacy/server/server.py b/diplomacy/server/server.py new file mode 100644 index 0000000..5763991 --- /dev/null +++ b/diplomacy/server/server.py @@ -0,0 +1,797 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Concret standalone server object. Manages and save server data and games on disk, send notifications, + receives requests and send responses. + + Example: + >>> from diplomacy import Server + >>> Server().start(port=1234) # If port is not given, a random port will be selected. + + You can interrupt server by sending a keyboard interrupt signal (Ctrl+C). + >>> from diplomacy import Server + >>> try: + >>> Server().start() + >>> except KeyboardInterrupt: + >>> print('Server interrupted.') + + You can also configure some server attributes when instantiating it: + >>> from diplomacy import Server + >>> server = Server(backup_delay_seconds=5) + >>> server.start() + + These are public configurable server attributes. They are saved on disk at each server backup: + - allow_user_registrations: (bool) indicate if server accepts users registrations + (default True) + - backup_delay_seconds: (int) number of seconds to wait between two consecutive full server backup on disk + (default 10 minutes) + - ping_seconds: (int) ping period used by server to check is connected sockets are alive. + - max_games: (int) maximum number of games server accepts to create. If there are at least such number of games on + server, server will not accept further game creation requests. If 0, no limit. + (default 0) + - remove_canceled_games: (bool) indicate if games must be deleted from server database when they are canceled + (default False) + +""" +import atexit +import logging +import os +import signal +import uuid + +import tornado +import tornado.web +from tornado import gen +from tornado.ioloop import IOLoop +from tornado.queues import Queue +from tornado.websocket import WebSocketClosedError + +import ujson as json + +import diplomacy.settings +from diplomacy.communication import notifications +from diplomacy.server.connection_handler import ConnectionHandler +from diplomacy.server.notifier import Notifier +from diplomacy.server.scheduler import Scheduler +from diplomacy.server.server_game import ServerGame +from diplomacy.server.users import Users +from diplomacy.engine.map import Map +from diplomacy.utils import common, exceptions, strings, constants + +LOGGER = logging.getLogger(__name__) + +def get_absolute_path(directory=None): + """ Return absolute path of given directory. + If given directory is None, return absolute path of current directory. + """ + return os.path.abspath(directory or os.getcwd()) + +def get_backup_filename(filename): + """ Return a backup filename from given filename (given filename with a special suffix). """ + return '%s.backup' % filename + +def save_json_on_disk(filename, json_dict): + """ Save given JSON dictionary into given filename and back-up previous file version if exists. """ + if os.path.exists(filename): + os.rename(filename, get_backup_filename(filename)) + with open(filename, 'w') as file: + json.dump(json_dict, file) + +def load_json_from_disk(filename): + """ Return a JSON dictionary loaded from given filename. + If JSON parsing fail for given filename, try to load JSON dictionary for a backup file (if present) + and rename backup file to given filename (backup file becomes current file versions). + :rtype: dict + """ + try: + with open(filename, 'rb') as file: + json_dict = json.load(file) + except ValueError as exception: + backup_filename = get_backup_filename(filename) + if not os.path.isfile(backup_filename): + raise exception + with open(backup_filename, 'rb') as backup_file: + json_dict = json.load(backup_file) + os.rename(backup_filename, filename) + return json_dict + +def ensure_path(folder_path): + """ Make sure given folder path exists and return given path. + Raises an exception if path does not exists, cannot be created or is not a folder. + """ + if not os.path.exists(folder_path): + LOGGER.info('Creating folder %s', folder_path) + os.makedirs(folder_path, exist_ok=True) + if not os.path.exists(folder_path) or not os.path.isdir(folder_path): + raise exceptions.FolderException(folder_path) + return folder_path + +class InterruptionHandler(): + """ Helper class used to save server when a system interruption signal is sent (e.g. KeyboardInterrupt). """ + __slots__ = ['server', 'previous_handler'] + + def __init__(self, server): + """ Initializer the handler. + :param server: server to save + """ + self.server = server # type: Server + self.previous_handler = signal.getsignal(signal.SIGINT) + + def handler(self, signum, frame): + """ Handler function. + :param signum: system signal received + :param frame: frame received + """ + if signum == signal.SIGINT: + self.server.backup_now(force=True) + if self.previous_handler: + self.previous_handler(signum, frame) + +class _ServerBackend(): + """ Class representing tornado objects used to run a server. Properties: + - port: (integer) port where server runs. + - application: tornado web Application object. + - http_server: tornado HTTP server object running server code. + - io_loop: tornado IO loop where server runs. + """ + #pylint: disable=too-few-public-methods + __slots__ = ['port', 'application', 'http_server', 'io_loop'] + + + def __init__(self): + """ Initialize server backend. """ + self.port = None + self.application = None + self.http_server = None + self.io_loop = None + +class Server(): + """ Server class. """ + __slots__ = ['data_path', 'games_path', 'available_maps', 'maps_mtime', 'notifications', + 'games_scheduler', 'allow_registrations', 'max_games', 'remove_canceled_games', 'users', 'games', + 'backup_server', 'backup_games', 'backup_delay_seconds', 'ping_seconds', + 'interruption_handler', 'backend', 'games_with_dummy_powers', 'dispatched_dummy_powers'] + + # Servers cache. + __cache__ = {} # {absolute path of working folder => Server} + + def __new__(cls, server_dir=None, **kwargs): + #pylint: disable=unused-argument + server_dir = get_absolute_path(server_dir) + if server_dir in cls.__cache__: + server = cls.__cache__[server_dir] + else: + server = object.__new__(cls) + return server + + def __init__(self, server_dir=None, **kwargs): + """ Initialize the server. + :param server_dir: path of folder in (from) which server data will be saved (loaded). + If None, working directory (where script is executed) will be used. + :param kwargs: (optional) values for some public configurable server attributes. + Given values will overwrite values saved on disk. + Server data is stored in folder `<working directory>/data`. + """ + + # File paths and attributes related to database. + server_dir = get_absolute_path(server_dir) + if server_dir in self.__class__.__cache__: + return + if not os.path.exists(server_dir) or not os.path.isdir(server_dir): + raise exceptions.ServerDirException(server_dir) + self.data_path = os.path.join(server_dir, 'data') + self.games_path = os.path.join(self.data_path, 'games') + + # Data in memory (not stored on disk). + self.notifications = Queue() + self.games_scheduler = Scheduler(1, self._process_game) + self.backup_server = None + self.backup_games = {} + self.interruption_handler = InterruptionHandler(self) + # Backend objects used to run server. If None, server is not yet started. + # Initialized when you call Server.start() (see method below). + self.backend = None # type: _ServerBackend + + # Database (stored on disk). + self.allow_registrations = True + self.max_games = 0 + self.remove_canceled_games = False + self.backup_delay_seconds = constants.DEFAULT_BACKUP_DELAY_SECONDS + self.ping_seconds = constants.DEFAULT_PING_SECONDS + self.users = None # type: Users # Users and administrators usernames. + self.available_maps = {} # type: dict{str, set()} # {"map_name" => set("map_power")} + self.maps_mtime = 0 # Latest maps modification date (used to manage maps cache in server object). + + # Server games loaded on memory (stored on disk). + # Saved separately (each game in one JSON file). + # Each game also stores tokens connected (player tokens, observer tokens, omniscient tokens). + self.games = {} # type: dict{str, ServerGame} + + # Dictionary mapping game IDs to dummy power names. + self.games_with_dummy_powers = {} # type: dict{str, set} + + # Dictionary mapping a game ID present in games_with_dummy_powers, to + # a couple of associated bot token and time when bot token was associated to this game ID. + # If there is no bot token associated, couple is (None, None). + self.dispatched_dummy_powers = {} # type: dict{str, tuple} + + # Load data on memory. + self._load() + + # If necessary, updated server configurable attributes from kwargs. + self.allow_registrations = bool(kwargs.pop(strings.ALLOW_REGISTRATIONS, self.allow_registrations)) + self.max_games = int(kwargs.pop(strings.MAX_GAMES, self.max_games)) + self.remove_canceled_games = bool(kwargs.pop(strings.REMOVE_CANCELED_GAMES, self.remove_canceled_games)) + self.backup_delay_seconds = int(kwargs.pop(strings.BACKUP_DELAY_SECONDS, self.backup_delay_seconds)) + self.ping_seconds = int(kwargs.pop(strings.PING_SECONDS, self.ping_seconds)) + assert not kwargs + LOGGER.debug('Ping : %s', self.ping_seconds) + LOGGER.debug('Backup delay: %s', self.backup_delay_seconds) + + # Add server on servers cache. + self.__class__.__cache__[server_dir] = self + + @property + def port(self): + """ Property: return port where this server currently runs, or None if server is not yet started. """ + return self.backend.port if self.backend else None + + def _load_available_maps(self): + """ Load a dictionary (self.available_maps) mapping every map name to a dict of map info. + for all maps available in diplomacy package. + """ + diplomacy_map_dir = os.path.join(diplomacy.settings.PACKAGE_DIR, strings.MAPS) + new_maps_mtime = self.maps_mtime + for filename in os.listdir(diplomacy_map_dir): + if filename.endswith('.map'): + map_filename = os.path.join(diplomacy_map_dir, filename) + map_mtime = os.path.getmtime(map_filename) + map_name = filename[:-4] + if map_name not in self.available_maps or map_mtime > self.maps_mtime: + # Either it's a new map file or map file was modified. + available_map = Map(map_name) + self.available_maps[map_name] = { + 'powers': set(available_map.powers), + 'supply_centers': set(available_map.scs), + 'loc_type': available_map.loc_type.copy(), + 'loc_abut': available_map.loc_abut.copy(), + 'aliases': available_map.aliases.copy() + } + new_maps_mtime = max(new_maps_mtime, map_mtime) + self.maps_mtime = new_maps_mtime + + def _get_server_data_filename(self): + """ Return path to server data file name (server.json, making sure that data folder exists. + Raises an exception if data folder does not exists and cannot be created. + """ + return os.path.join(ensure_path(self.data_path), 'server.json') + + def _load(self): + """ Load database from disk. """ + LOGGER.info("Loading database.") + ensure_path(self.data_path) # <server dir>/data + ensure_path(self.games_path) # <server dir>/data/games + server_data_filename = self._get_server_data_filename() # <server dir>/data/server.json + if os.path.exists(server_data_filename): + LOGGER.info("Loading server.json.") + server_info = load_json_from_disk(server_data_filename) + self.allow_registrations = server_info[strings.ALLOW_REGISTRATIONS] + self.backup_delay_seconds = server_info[strings.BACKUP_DELAY_SECONDS] + self.ping_seconds = server_info[strings.PING_SECONDS] + self.max_games = server_info[strings.MAX_GAMES] + self.remove_canceled_games = server_info[strings.REMOVE_CANCELED_GAMES] + self.users = Users.from_dict(server_info[strings.USERS]) + self.available_maps = server_info[strings.AVAILABLE_MAPS] + self.maps_mtime = server_info[strings.MAPS_MTIME] + # games and map are loaded from disk. + else: + LOGGER.info("Creating server.json.") + self.users = Users() + self.backup_now(force=True) + # Add default accounts. + for (username, password) in ( + ('admin', 'password'), + (constants.PRIVATE_BOT_USERNAME, constants.PRIVATE_BOT_PASSWORD) + ): + if not self.users.has_username(username): + self.users.add_user(username, common.hash_password(password)) + # Set default admin account. + self.users.add_admin('admin') + + self._load_available_maps() + + LOGGER.info('Server loaded.') + + def _backup_server_data_now(self, force=False): + """ Save latest backed-up version of server data on disk. This does not save games. + :param force: if True, force to save current server data even if it was not modified recently. + """ + if force: + self.save_data() + if self.backup_server: + save_json_on_disk(self._get_server_data_filename(), self.backup_server) + self.backup_server = None + LOGGER.info("Saved server.json.") + + def _backup_games_now(self, force=False): + """ Save latest backed-up versions of loaded games on disk. + :param force: if True, force to save all games currently loaded in memory + even if they were not modified recently. + """ + ensure_path(self.games_path) + if force: + for server_game in self.games.values(): + self.save_game(server_game) + for game_id, game_dict in self.backup_games.items(): + game_path = os.path.join(self.games_path, '%s.json' % game_id) + save_json_on_disk(game_path, game_dict) + LOGGER.info('Game data saved: %s', game_id) + self.backup_games.clear() + + def backup_now(self, force=False): + """ Save backup of server data and loaded games immediately. + :param force: if True, force to save server data and all loaded games even if there are no recent changes. + """ + self._backup_server_data_now(force=force) + self._backup_games_now(force=force) + + @gen.coroutine + def _process_game(self, server_game): + """ Process given game and send relevant notifications. + :param server_game: server game to process + :return: A boolean indicating if we must stop game. + :type server_game: ServerGame + """ + LOGGER.debug('Processing game %s (status %s).', server_game.game_id, server_game.status) + previous_phase_data, current_phase_data, kicked_powers = server_game.process() + self.save_game(server_game) + + if previous_phase_data is None and kicked_powers is None: + # Game must be unscheduled immediately. + return True + + notifier = Notifier(self) + # In any case, we notify game tokens about changes in power controllers. + yield notifier.notify_game_powers_controllers(server_game) + + if kicked_powers: + # Game was not processed because of kicked powers. + # We notify those kicked powers and game must be unscheduled immediately. + kicked_addresses = [(power_name, token) + for (power_name, tokens) in kicked_powers.items() + for token in tokens] + # Notify kicked players. + notifier.notify_game_addresses( + server_game.game_id, + kicked_addresses, + notifications.PowersControllers, + powers=server_game.get_controllers(), + timestamps=server_game.get_controllers_timestamps() + ) + return True + + # Game was processed normally. + # Send game updates to powers, observers and omniscient observers. + yield notifier.notify_game_processed(server_game, previous_phase_data, current_phase_data) + return not server_game.is_game_active + + @gen.coroutine + def _task_save_database(self): + """ IO loop callable: save database and loaded games periodically. + Data to save are checked every BACKUP_DELAY_SECONDS seconds. + """ + LOGGER.info('Waiting for save events.') + while True: + yield gen.sleep(self.backup_delay_seconds) + self.backup_now() + + @gen.coroutine + def _task_send_notifications(self): + """ IO loop callback: consume notifications and send it. """ + LOGGER.info('Waiting for notifications to send.') + while True: + connection_handler, notification = yield self.notifications.get() + try: + yield connection_handler.write_message(notification.json()) + except WebSocketClosedError: + LOGGER.error('Websocket was closed while sending a notification.') + finally: + self.notifications.task_done() + + def set_tasks(self, io_loop: IOLoop): + """ Set server callbacks on given IO loop. Must be called once per server before starting IO loop. """ + io_loop.add_callback(self._task_save_database) + io_loop.add_callback(self._task_send_notifications) + # These both coroutines are used to manage games. + io_loop.add_callback(self.games_scheduler.process_tasks) + io_loop.add_callback(self.games_scheduler.schedule) + # Set callback on KeyboardInterrupt. + signal.signal(signal.SIGINT, self.interruption_handler.handler) + atexit.register(self.backup_now) + + def start(self, port=None, io_loop=None): + """ Start server if not yet started. Raise an exception if server is already started. + :param port: (optional) port where server must run. If not provided, try to start on a random + selected port. Use property `port` to get current server port. + :param io_loop: (optional) tornado IO lopp where server must run. If not provided, get + default IO loop instance (tornado.ioloop.IOLoop.instance()). + """ + if self.backend is not None: + raise exceptions.DiplomacyException('Server is already running on port %s.' % self.backend.port) + if port is None: + port = 8432 + if io_loop is None: + io_loop = tornado.ioloop.IOLoop.instance() + handlers = [ + tornado.web.url(r"/", ConnectionHandler, {'server': self}), + ] + settings = { + 'cookie_secret': common.generate_token(), + 'xsrf_cookies': True, + 'websocket_ping_interval': self.ping_seconds, + 'websocket_ping_timeout': 2 * self.ping_seconds, + 'websocket_max_message_size': 64 * 1024 * 1024 + } + self.backend = _ServerBackend() + self.backend.application = tornado.web.Application(handlers, **settings) + self.backend.http_server = self.backend.application.listen(port) + self.backend.io_loop = io_loop + self.backend.port = port + self.set_tasks(io_loop) + LOGGER.info('Running on port %d', self.backend.port) + io_loop.start() + + def get_game_indices(self): + """ Iterate over all game indices in server database. + Convenient method to iterate over all server games (by calling load_game() on each game index). + """ + for game_id in self.games: + yield game_id + if os.path.isdir(self.games_path): + for filename in os.listdir(self.games_path): + if filename.endswith('.json'): + game_id = filename[:-5] + if game_id not in self.games: + yield game_id + + def count_server_games(self): + """ Return number of server games in server database. """ + count = 0 + if os.path.isdir(self.games_path): + for filename in os.listdir(self.games_path): + if filename.endswith('.json'): + count += 1 + return count + + def save_data(self): + """ Update on-memory backup of server data. """ + self.backup_server = { + strings.ALLOW_REGISTRATIONS: self.allow_registrations, + strings.BACKUP_DELAY_SECONDS: self.backup_delay_seconds, + strings.PING_SECONDS: self.ping_seconds, + strings.MAX_GAMES: self.max_games, + strings.REMOVE_CANCELED_GAMES: self.remove_canceled_games, + strings.USERS: self.users.to_dict(), + strings.AVAILABLE_MAPS: self.available_maps, + strings.MAPS_MTIME: self.maps_mtime, + } + + def save_game(self, server_game): + """ Update on-memory version of given server game. + :param server_game: server game + :type server_game: ServerGame + """ + self.backup_games[server_game.game_id] = server_game.to_dict() + # Check dummy powers for a game every time we have to save it. + self.register_dummy_power_names(server_game) + + def register_dummy_power_names(self, server_game): + """ Update internal registry of dummy power names waiting for orders + for given server games. + :param server_game: server game to check + :type server_game: ServerGame + """ + updated = False + if server_game.is_game_active or server_game.is_game_paused: + dummy_power_names = [] + for power_name in server_game.get_dummy_power_names(): + power = server_game.get_power(power_name) + if power.is_dummy() and not power.is_eliminated() and not power.does_not_wait(): + # This dummy power needs either orders, or wait flag to be set to False. + dummy_power_names.append(power_name) + if dummy_power_names: + # Update registry of dummy powers. + self.games_with_dummy_powers[server_game.game_id] = dummy_power_names + # Every time we update registry of dummy powers, + # then we also update bot time in registry of dummy powers associated to bot tokens. + bot_token, _ = self.dispatched_dummy_powers.get(server_game.game_id, (None, None)) + self.dispatched_dummy_powers[server_game.game_id] = (bot_token, common.timestamp_microseconds()) + updated = True + if not updated: + # Registry not updated for this game, meaning that there is no + # dummy powers waiting for orders or 'no wait' for this game. + self.games_with_dummy_powers.pop(server_game.game_id, None) + # We remove game from registry of dummy powers associated to bot tokens only if game is terminated. + # Otherwise, game will remain associated to a previous bot token, until bot failed to order powers. + if server_game.is_game_completed or server_game.is_game_canceled: + self.dispatched_dummy_powers.pop(server_game.game_id, None) + + def get_dummy_waiting_power_names(self, buffer_size, bot_token): + """ Return names of dummy powers waiting for orders for current loaded games. + This query is allowed only for bot tokens. + :param buffer_size: maximum number of powers queried. + :param bot_token: bot token + :return: a dictionary mapping game IDs to lists of power names. + """ + if self.users.get_name(bot_token) != constants.PRIVATE_BOT_USERNAME: + raise exceptions.ResponseException('Invalid bot token %s' % bot_token) + selected_size = 0 + selected_games = {} + for game_id in sorted(list(self.games_with_dummy_powers.keys())): + registered_token, registered_time = self.dispatched_dummy_powers[game_id] + if registered_token is not None: + time_elapsed_seconds = (common.timestamp_microseconds() - registered_time) / 1000000 + if time_elapsed_seconds > constants.PRIVATE_BOT_TIMEOUT_SECONDS or registered_token == bot_token: + # This game still has dummy powers but time allocated to previous bot token is over. + # Forget previous bot token. + registered_token = None + if registered_token is None: + # This game is not associated to any bot token. + # Let current bot token handle it if buffer size is not reached. + dummy_power_names = self.games_with_dummy_powers[game_id] + nb_powers = len(dummy_power_names) + if selected_size + nb_powers > buffer_size: + # Buffer size would be exceeded. We stop to collect games now. + break + # Otherwise we collect this game. + selected_games[game_id] = dummy_power_names + selected_size += nb_powers + self.dispatched_dummy_powers[game_id] = (bot_token, common.timestamp_microseconds()) + return selected_games + + def has_game_id(self, game_id): + """ Return True if server database contains such game ID. """ + if game_id in self.games: + return True + expected_game_path = os.path.join(self.games_path, '%s.json' % game_id) + return os.path.exists(expected_game_path) and os.path.isfile(expected_game_path) + + def load_game(self, game_id): + """ Return a game matching given game ID from server database. + Raise an exception if such game does not exists. + If such game is already stored in server object, return it. + Else, load it from disk but ** does not store it in server object **. + To load and immediately store a game object in server object, please use method get_game(). + Method load_game() is convenient where you want to iterate over all games in server database + without taking memory space. + :param game_id: ID of game to load. + :return: a ServerGame object + :rtype: ServerGame + """ + if game_id in self.games: + return self.games[game_id] + game_filename = os.path.join(ensure_path(self.games_path), '%s.json' % game_id) + if not os.path.isfile(game_filename): + raise exceptions.GameIdException() + try: + server_game = ServerGame.from_dict(load_json_from_disk(game_filename)) # type: ServerGame + server_game.server = self + server_game.filter_usernames(self.users.has_username) + server_game.filter_tokens(self.users.has_token) + return server_game + except ValueError as exc: + # Error occurred while parsing JSON file: bad JSON file. + try: + os.remove(game_filename) + finally: + # This should be an internal server error. + raise exc + + def add_new_game(self, server_game): + """ Add a new game data on server in memory. This does not save the game on disk. + :type server_game: ServerGame + """ + self.games[server_game.game_id] = server_game + + def get_game(self, game_id): + """ Return game saved on server matching given game ID. Raise an exception if game ID not found. + Return game if already loaded on memory, else load it from disk, store it and return it. + :param game_id: ID of game to load. + :return: a ServerGame object. + :rtype: ServerGame + """ + server_game = self.load_game(game_id) + if game_id not in self.games: + LOGGER.debug('Game loaded: %s', game_id) + # Check dummy powers for this game as soon as it's loaded from disk. + self.register_dummy_power_names(server_game) + self.games[server_game.game_id] = server_game + # We have just loaded game from disk. Start it if necessary. + if not server_game.start_master and server_game.has_expected_controls_count(): + # We may have to start game. + stop = False + if server_game.does_not_wait(): + # We must process game. + process_result = server_game.process() + stop = process_result is None or process_result[-1] + self.save_game(server_game) + if not stop: + LOGGER.debug('Game loaded and scheduled: %s', server_game.game_id) + self.schedule_game(server_game) + return server_game + + def delete_game(self, server_game): + """ Delete given game from server (both from memory and disk). + :param server_game: game to delete + :type server_game: ServerGame + """ + if not (server_game.is_game_canceled or server_game.is_game_completed): + server_game.set_status(strings.CANCELED) + game_filename = os.path.join(self.games_path, '%s.json' % server_game.game_id) + if os.path.isfile(game_filename): + os.remove(game_filename) + self.games.pop(server_game.game_id, None) + self.games_with_dummy_powers.pop(server_game.game_id, None) + self.dispatched_dummy_powers.pop(server_game.game_id, None) + + @gen.coroutine + def schedule_game(self, server_game): + """ Add a game to scheduler only if game has a deadline and is not already scheduled. + To add games without deadline, use force_game_processing(). + :param server_game: game + :type server_game: ServerGame + """ + if not (yield self.games_scheduler.has_data(server_game)) and server_game.deadline: + yield self.games_scheduler.add_data(server_game, server_game.deadline) + + @gen.coroutine + def unschedule_game(self, server_game): + """ Remove a game from scheduler. + :param server_game: game + :type server_game: ServerGame + """ + if (yield self.games_scheduler.has_data(server_game)): + yield self.games_scheduler.remove_data(server_game) + + @gen.coroutine + def force_game_processing(self, server_game): + """ Add a game to scheduler to be processed as soon as possible. + Use this method instead of schedule_game() to explicitly add games with null deadline. + :param server_game: game + :type server_game: ServerGame + """ + yield self.games_scheduler.no_wait(server_game, server_game.deadline, lambda g: g.does_not_wait()) + + def start_game(self, server_game): + """ Start given server game. + :param server_game: server game + :type server_game: ServerGame + """ + server_game.set_status(strings.ACTIVE) + self.schedule_game(server_game) + Notifier(self).notify_game_status(server_game) + + def stop_game_if_needed(self, server_game): + """ Stop game if it has not required number of controlled powers. Notify game if status changed. + :param server_game: game to check + :param server_game: game + :type server_game: ServerGame + """ + if server_game.is_game_active and ( + server_game.count_controlled_powers() < server_game.get_expected_controls_count()): + server_game.set_status(strings.FORMING) + self.unschedule_game(server_game) + Notifier(self).notify_game_status(server_game) + + def user_is_master(self, username, server_game): + """ Return True if given username is a game master for given game data. + :param username: username + :param server_game: game data + :return: a boolean + :type server_game: ServerGame + :rtype: bool + """ + return self.users.has_admin(username) or server_game.is_moderator(username) + + def user_is_omniscient(self, username, server_game): + """ Return True if given username is omniscient for given game data. + :param username: username + :param server_game: game data + :return: a boolean + :type server_game: ServerGame + :rtype: bool + """ + return self.users.has_admin(username) or server_game.is_moderator(username) or server_game.is_omniscient( + username) + + def token_is_master(self, token, server_game): + """ Return True if given token is a master token for given game data. + :param token: token + :param server_game: game data + :return: a boolean + :type server_game: ServerGame + :rtype: bool + """ + return self.users.has_token(token) and self.user_is_master(self.users.get_name(token), server_game) + + def token_is_omniscient(self, token, server_game): + """ Return True if given token is omniscient for given game data. + :param token: token + :param server_game: game data + :return: a boolean + :type server_game: ServerGame + :rtype: bool + """ + return self.users.has_token(token) and self.user_is_omniscient(self.users.get_name(token), server_game) + + def create_game_id(self): + """ Create and return a game ID not already used by a game in server database. """ + game_id = str(uuid.uuid4()) + while self.has_game_id(game_id): + game_id = str(uuid.uuid4()) + return game_id + + def remove_token(self, token): + """ Disconnect given token from related user and loaded games. + Stop related games if needed, e.g. if a game does not have anymore + expected number of controlled powers. + """ + self.users.disconnect_token(token) + for server_game in self.games.values(): # type: ServerGame + server_game.remove_token(token) + self.stop_game_if_needed(server_game) + self.save_game(server_game) + self.save_data() + + def assert_token(self, token, connection_handler): + """ Check if given token is associated to an user, check if token is still valid, and link token to given + connection handler. If any step failed, raise an exception. + :param token: token to check + :param connection_handler: connection handler associated to this token + """ + if not self.users.has_token(token): + raise exceptions.TokenException() + if self.users.token_is_alive(token): + self.users.relaunch_token(token) + self.save_data() + else: + # Logout on server side and raise exception (invalid token). + LOGGER.error('Token too old %s', token) + self.remove_token(token) + raise exceptions.TokenException() + self.users.attach_connection_handler(token, connection_handler) + + def assert_admin_token(self, token): + """ Check if given token is an admin token. Raise an exception on error. """ + if not self.users.token_is_admin(token): + raise exceptions.AdminTokenException() + + def assert_master_token(self, token, server_game): + """ Check if given token is a master token for given game data. Raise an exception on error. + :param token: token + :param server_game: game data + :type server_game: ServerGame + """ + if not self.token_is_master(token, server_game): + raise exceptions.GameMasterTokenException() + + def cannot_create_more_games(self): + """ Return True if server can not accept new games. """ + return self.max_games and self.count_server_games() >= self.max_games + + def get_map(self, map_name): + """ Return map power names for given map name. """ + return self.available_maps.get(map_name, None) diff --git a/diplomacy/server/server_game.py b/diplomacy/server/server_game.py new file mode 100644 index 0000000..6ea349e --- /dev/null +++ b/diplomacy/server/server_game.py @@ -0,0 +1,465 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Server game class. """ +from diplomacy.engine.game import Game +from diplomacy.engine.message import GLOBAL, Message, OBSERVER, OMNISCIENT, SYSTEM +from diplomacy.engine.power import Power +from diplomacy.utils import exceptions, parsing, strings +from diplomacy.utils.game_phase_data import GamePhaseData + +class ServerGame(Game): + """ ServerGame class. Properties: + - omniscient_usernames (only for server games): + set of usernames allowed to be omniscient observers for this game. + - moderator_usernames (only for server games): + set of usernames allowed to be moderators for this game. + - observer (only for server games): + special Power object (diplomacy.Power) used to manage observer tokens. + - omniscient (only for server games): + special Power object (diplomacy.Power) used to manage omniscient tokens. + """ + __slots__ = ['server', 'omniscient_usernames', 'moderator_usernames', 'observer', 'omniscient'] + model = parsing.update_model(Game.model, { + strings.MODERATOR_USERNAMES: parsing.DefaultValueType(parsing.SequenceType(str, sequence_builder=set), ()), + strings.OBSERVER: parsing.OptionalValueType(parsing.JsonableClassType(Power)), + strings.OMNISCIENT: parsing.OptionalValueType(parsing.JsonableClassType(Power)), + strings.OMNISCIENT_USERNAMES: parsing.DefaultValueType(parsing.SequenceType(str, sequence_builder=set), ()), + }) + + def __init__(self, **kwargs): + # Reference to a Server instance. + self.server = None # type: diplomacy.Server + self.omniscient_usernames = None # type: set + self.moderator_usernames = None # type: set + self.observer = None # type: Power + self.omniscient = None # type: Power + + super(ServerGame, self).__init__(**kwargs) + assert self.is_server_game() + + # Initialize special powers. + self.observer = self.observer or Power(self, name=strings.OBSERVER_TYPE) + self.omniscient = self.omniscient or Power(self, name=strings.OMNISCIENT_TYPE) + self.observer.set_controlled(strings.OBSERVER_TYPE) + self.omniscient.set_controlled(strings.OBSERVER_TYPE) + + # Server-only methods. + + def get_related_power_names(self, power_name): + """ Return list of power names controlled by the controlled of given power name. """ + related_power_names = [] + if self.has_power(power_name): + related_power_names = [power_name] + related_power = self.get_power(power_name) + if related_power.is_controlled(): + related_power_names = self.get_controlled_power_names(related_power.get_controller()) + return related_power_names + + def filter_phase_data(self, phase_data, role, is_current): + """ Return a filtered version of given phase data for given gam role. + :param phase_data: GamePhaseData object to filter. + :param role: game role to filter phase data for. + :param is_current: Boolean. Indicate if given phase data is for a current phase (True), or for a pase phase. + :return: a new GamePhaseData object suitable for given game role. + :type phase_data: GamePhaseData + """ + if role == strings.OMNISCIENT_TYPE: + # Nothing to filter. + return phase_data + if role == strings.OBSERVER_TYPE: + # Filter messages. + return GamePhaseData(name=phase_data.name, + state=phase_data.state, + orders=phase_data.orders, + results=phase_data.results, + messages=self.filter_messages(phase_data.messages, role)) + # Filter for power roles. + related_power_names = self.get_related_power_names(role) + # Filter messages. + messages = self.filter_messages(phase_data.messages, related_power_names) + # We filter orders only if phase data is for a current phase. + if is_current: + orders = {power_name: phase_data.orders[power_name] + for power_name in related_power_names + if power_name in phase_data.orders} + else: + orders = phase_data.orders + # results don't need to be filtered: it should be provided empty for current phase, + # and it should be kept for a past phase/ + return GamePhaseData(name=phase_data.name, + state=phase_data.state, + orders=orders, + messages=messages, + results=phase_data.results) + + def game_can_start(self): + """ Return True if server game can start. A game can start if all followings conditions are satisfied: + - Game has not yet started. + - Game can start automatically (no rule START_MASTER). + - Game has expected number of controlled powers. + :return: a boolean + :rtype: bool + """ + return self.is_game_forming and not self.start_master and self.has_expected_controls_count() + + def get_messages(self, game_role, timestamp_from=None, timestamp_to=None): + """ Return a filtered dict of current messages for given output game role. + See method filter_messages() about parameters. + """ + return self.filter_messages(self.messages, game_role, timestamp_from, timestamp_to) + + def get_message_history(self, game_role): + """ Return a filtered dict of whole message history for given game role. """ + return {str(short_phase): self.filter_messages(messages, game_role) + for short_phase, messages in self.message_history.items()} + + def get_user_power_names(self, username): + """ Return list of power names controlled by given user name. """ + return [power.name for power in self.powers.values() if power.is_controlled_by(username)] + + def new_system_message(self, recipient, body): + """ Create a system message (immediately dated) to be sent by server and add it to message history. + To be used only by server game. + :param recipient: recipient description (string). Either: + - a power name. + - 'GLOBAL' (all game tokens) + - 'OBSERVER' (all special tokens [observers and omniscient observers]) + - 'OMNISCIENT' (all omniscient tokens only) + :param body: message body (string). + :return: a new GameMessage object. + :rtype: Message + """ + assert (recipient in {GLOBAL, OBSERVER, OMNISCIENT} + or self.has_power(recipient)) + message = Message(phase=self.current_short_phase, sender=SYSTEM, recipient=recipient, message=body) + # Message timestamp will be generated when adding message. + self.add_message(message) + return message + + def as_power_game(self, power_name): + """ Return a player game data object copy of this game for given power name. """ + for_username = self.get_power(power_name).get_controller() + game = Game.from_dict(self.to_dict()) + game.controlled_powers = self.get_controlled_power_names(for_username) + game.error = [] + game.message_history = self.get_message_history(power_name) + game.messages = self.get_messages(power_name) + game.observer_level = self.get_observer_level(for_username) + game.phase_abbr = game.current_short_phase + related_power_names = self.get_related_power_names(power_name) + for power in game.powers.values(): # type: Power + power.role = power.name + power.tokens.clear() + if power.name not in related_power_names: + power.vote = strings.NEUTRAL + power.orders.clear() + game.role = power_name + return game + + def as_omniscient_game(self, for_username): + """ Return an omniscient game data object copy of this game. """ + game = Game.from_dict(self.to_dict()) + game.controlled_powers = self.get_controlled_power_names(for_username) + game.message_history = self.get_message_history(strings.OMNISCIENT_TYPE) + game.messages = self.get_messages(strings.OMNISCIENT_TYPE) + game.observer_level = self.get_observer_level(for_username) + game.phase_abbr = game.current_short_phase + for power in game.powers.values(): # type: Power + power.role = strings.OMNISCIENT_TYPE + power.tokens.clear() + game.role = strings.OMNISCIENT_TYPE + return game + + def as_observer_game(self, for_username): + """ Return an observer game data object copy of this game. """ + game = Game.from_dict(self.to_dict()) + game.controlled_powers = self.get_controlled_power_names(for_username) + game.error = [] + game.message_history = self.get_message_history(strings.OBSERVER_TYPE) + game.messages = self.get_messages(strings.OBSERVER_TYPE) + game.observer_level = self.get_observer_level(for_username) + game.phase_abbr = game.current_short_phase + for power in game.powers.values(): # type: Power + power.role = strings.OBSERVER_TYPE + power.tokens.clear() + power.vote = strings.NEUTRAL + game.role = strings.OBSERVER_TYPE + return game + + def cast(self, role, for_username): + """ Return a copy of this game for given role (either observer role, omniscient role or a power role). """ + assert strings.role_is_special(role) or self.has_power(role) + if role == strings.OBSERVER_TYPE: + return self.as_observer_game(for_username) + if role == strings.OMNISCIENT_TYPE: + return self.as_omniscient_game(for_username) + return self.as_power_game(role) + + def is_controlled_by(self, power_name, username): + """ (for server game) Return True if given power name is controlled by given username. """ + return self.get_power(power_name).is_controlled_by(username) + + def get_observer_level(self, username): + """ Return the highest observation level allowed for given username. + :param username: name of user to get observation right + :return: either 'master_type', 'omniscient_type', 'observer_type' or None. + """ + if (self.server and self.server.users.has_admin(username)) or self.is_moderator(username): + return strings.MASTER_TYPE + if self.is_omniscient(username): + return strings.OMNISCIENT_TYPE + if not self.no_observations: + return strings.OBSERVER_TYPE + return None + + def get_reception_addresses(self): + """ Generate addresses (couple [power name, token]) of all users implied in this game. """ + for power in self.powers.values(): # type: Power + for token in power.tokens: + yield (power.name, token) + for token in self.observer.tokens: + yield (self.observer.name, token) + for token in self.omniscient.tokens: + yield (self.omniscient.name, token) + + def get_special_addresses(self): + """ Generate addresses (couples [power name, token]) of + omniscient observers and simple observers of this game. """ + for power in (self.omniscient, self.observer): + for token in power.tokens: + yield (power.name, token) + + def get_observer_addresses(self): + """ Generate addresses (couples [power name, token]) of observers of this game. """ + for token in self.observer.tokens: + yield (self.observer.name, token) + + def get_omniscient_addresses(self): + """ Generate addresses (couples [power name, token]) of omniscient observers of this game. """ + for token in self.omniscient.tokens: + yield (self.omniscient.name, token) + + def get_special_token_role(self, token): + """ Return role name (either OBSERVER_TYPE or OMNISCIENT_TYPE) for given special token. """ + if self.has_omniscient_token(token): + return strings.OMNISCIENT_TYPE + if self.has_observer_token(token): + return strings.OBSERVER_TYPE + raise exceptions.DiplomacyException('Unknown special token in game %s' % self.game_id) + + def get_power_addresses(self, power_name): + """ Generate addresses (couples [power name, token]) of user controlling given power name. """ + for token in self.get_power(power_name).tokens: + yield (power_name, token) + + def has_player(self, username): + """ (for server game) Return True if given username controls any map power. """ + return any(power.is_controlled_by(username) for power in self.powers.values()) + + def has_token(self, token): + """ Return True if game has given token (either observer, omniscient or player). """ + return self.omniscient.has_token(token) or self.observer.has_token(token) or any( + power.has_token(token) for power in self.powers.values()) + + def has_observer_token(self, token): + """ Return True if game has given observer token. """ + return self.observer.has_token(token) + + def has_omniscient_token(self, token): + """ Return True if game has given omniscient observer token. """ + return self.omniscient.has_token(token) + + def has_player_token(self, token): + """ Return True if game has given player token. """ + return any(power.has_token(token) for power in self.powers.values()) + + def power_has_token(self, power_name, token): + """ Return True if given power has given player token. + :param power_name: name of power to check. + :param token: token to look for. + :return: a boolean + """ + return self.get_power(power_name).has_token(token) + + def add_omniscient_token(self, token): + """ Set given token as omniscient token. """ + if self.observer.has_token(token): + raise exceptions.ResponseException('Token already registered as observer.') + if self.has_player_token(token): + raise exceptions.ResponseException('Token already registered as player.') + self.omniscient.add_token(token) + + def add_observer_token(self, token): + """ Set given token as observer token. """ + if self.omniscient.has_token(token): + raise exceptions.ResponseException('Token already registered as omniscient.') + if self.has_player_token(token): + raise exceptions.ResponseException('Token already registered as player.') + self.observer.add_token(token) + + def transfer_special_token(self, token): + """ Move given token from a special case to another (observer -> omniscient or omniscient -> observer). """ + if self.has_observer_token(token): + self.remove_observer_token(token) + self.add_omniscient_token(token) + elif self.has_omniscient_token(token): + self.remove_omniscient_token(token) + self.add_observer_token(token) + + def control(self, power_name, username, token): + """ Control given power name with given username via given token. """ + if self.observer.has_token(token): + raise exceptions.ResponseException('Token already registered as observer.') + if self.omniscient.has_token(token): + raise exceptions.ResponseException('Token already registered as omniscient.') + power = self.get_power(power_name) # type: Power + if power.is_controlled() and not power.is_controlled_by(username): + raise exceptions.ResponseException('Power already controlled by another user.') + power.set_controlled(username) + power.add_token(token) + + def remove_observer_token(self, token): + """ Remove given observer token. """ + self.observer.remove_tokens([token]) + + def remove_omniscient_token(self, token): + """ Remove given omniscient token. """ + self.omniscient.remove_tokens([token]) + + def remove_special_token(self, special_name, token): + """ Remove given token from given special power name (either __OBSERVER__ or __OMNISCIENT__). """ + if special_name == self.observer.name: + self.remove_observer_token(token) + else: + assert special_name == self.omniscient.name + self.remove_omniscient_token(token) + + def remove_all_tokens(self): + """ Remove all connected tokens from this game. """ + self.observer.tokens.clear() + self.omniscient.tokens.clear() + for power in self.powers.values(): + power.tokens.clear() + + def remove_token(self, token): + """ Remove token from this game. """ + for power in self.powers.values(): # type: Power + power.remove_tokens([token]) + for special_power in (self.observer, self.omniscient): + special_power.remove_tokens([token]) + + def is_moderator(self, username): + """ Return True if given username is a moderator of this game. """ + return username in self.moderator_usernames + + def is_omniscient(self, username): + """ Return True if given username is allowed to be an omniscient observer of this game. """ + return username in self.omniscient_usernames + + def promote_moderator(self, username): + """ Allow given username to be a moderator of this game. """ + self.moderator_usernames.add(username) + + def promote_omniscient(self, username): + """ Allow given username to be an omniscient observer of this game. """ + self.omniscient_usernames.add(username) + + def demote_moderator(self, username): + """ Remove given username from allowed moderators. """ + if username in self.moderator_usernames: + self.moderator_usernames.remove(username) + + def demote_omniscient(self, username): + """ Remove given username from allowed omniscient observers. """ + if username in self.omniscient_usernames: + self.omniscient_usernames.remove(username) + + def filter_usernames(self, filter_function): + """ Remove each omniscient username, moderator username and player controller that does not match given + filter function (if filter_function(username) is False). + :param filter_function: a callable receiving a username and returning a boolean. + :return: an integer, either: + * 0: nothing changed. + * -1: something changed, but no player controllers removed. + * 1: something changed, and some player controllers were removed. + So, if 1 is returned, there are new dummy powers in the game (some notifications may need to be sent). + """ + n_kicked_players = 0 + n_kicked_omniscients = len(self.omniscient_usernames) + n_kicked_moderators = len(self.moderator_usernames) + self.omniscient_usernames = set(username for username in self.omniscient_usernames if filter_function(username)) + self.moderator_usernames = set(username for username in self.moderator_usernames if filter_function(username)) + for power in self.powers.values(): + if power.is_controlled() and not filter_function(power.get_controller()): + power.set_controlled(None) + n_kicked_players += 1 + n_kicked_omniscients -= len(self.omniscient_usernames) + n_kicked_moderators -= len(self.moderator_usernames) + if n_kicked_players: + return 1 + if n_kicked_moderators or n_kicked_omniscients: + return -1 + return 0 + + def filter_tokens(self, filter_function): + """ Remove from this game any token not matching given filter function (if filter_function(token) is False).""" + self.observer.remove_tokens([token for token in self.observer.tokens if not filter_function(token)]) + self.omniscient.remove_tokens([token for token in self.omniscient.tokens if not filter_function(token)]) + for power in self.powers.values(): # type: Power + power.remove_tokens([token for token in power.tokens if not filter_function(token)]) + + def process(self): + """ Process current game phase and move forward to next phase. + :return: a triple containing: + - previous game state (before the processing) + - current game state (after processing and game updates) + - A dictionary mapping kicked power names to tokens previously associated to these powers. + Useful to notify kicked users as they will be not registered in game anymore. + If game was not active, triple is (None, None, None). + If game kicked powers, only kicked powers dict is returned: (None, None, kicked powers). + If game was correctly processed, only states are returned: (prev, curr, None). + """ + if not self.is_game_active: + return None, None, None + # Kick powers if necessary. + all_orderable_locations = self.get_orderable_locations() + kicked_powers = {} + for power in self.powers.values(): + if (power.is_controlled() + and not power.order_is_set + and not self.civil_disorder + and all_orderable_locations[power.name]): + # This controlled power has not submitted orders, we have not rule CIVIL_DISORDER, + # and this power WAS allowed to submit orders for this phase. + # We kick such power. + kicked_powers[power.name] = set(power.tokens) + power.set_controlled(None) + + if kicked_powers: + # Some powers were kicked from an active game before processing. + # This game must be stopped and cannot be processed. We return info about kicked powers. + self.set_status(strings.FORMING) + return None, None, kicked_powers + + # Process game and retrieve previous state. + previous_phase_data = super(ServerGame, self).process() + if self.count_controlled_powers() < self.get_expected_controls_count(): + # There is no more enough controlled powers, we should stop game. + self.set_status(strings.FORMING) + + # Return process results: previous phase data, current phase data, and None for no kicked powers. + return previous_phase_data, self.get_phase_data(), None diff --git a/diplomacy/server/user.py b/diplomacy/server/user.py new file mode 100644 index 0000000..cfb6ad4 --- /dev/null +++ b/diplomacy/server/user.py @@ -0,0 +1,37 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" User object, defined with a username and a hashed password. """ +from diplomacy.utils import strings +from diplomacy.utils.common import is_valid_password +from diplomacy.utils.jsonable import Jsonable + +class User(Jsonable): + """ User class. """ + __slots__ = ['username', 'password_hash'] + model = { + strings.USERNAME: str, + strings.PASSWORD_HASH: str + } + + def __init__(self, **kwargs): + self.username = None + self.password_hash = None + super(User, self).__init__(**kwargs) + + def is_valid_password(self, password): + """ Return True if given password matches user hashed password. """ + return is_valid_password(password, self.password_hash) diff --git a/diplomacy/server/users.py b/diplomacy/server/users.py new file mode 100644 index 0000000..d1c8ca0 --- /dev/null +++ b/diplomacy/server/users.py @@ -0,0 +1,234 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Helper class to manage user accounts and connections on server side. + + A user is associated to 0 or more connected tokens, + and each connected token is associated to at most 1 connection handler. + + When a connection handler is closed or invalidated, + related tokens are kept and may be further associated to new connection handlers. + + Tokens are effectively deleted when they expire after TOKEN_LIFETIME_SECONDS seconds since last token usage. +""" +import logging + +from diplomacy.server.user import User +from diplomacy.utils import common, parsing, strings +from diplomacy.utils.common import generate_token +from diplomacy.utils.jsonable import Jsonable + +LOGGER = logging.getLogger(__name__) + +# Token lifetime in seconds: default 24hours. +TOKEN_LIFETIME_SECONDS = 24 * 60 * 60 + +class Users(Jsonable): + """ Users class. Properties: + - users: dictionary mapping usernames to User object.s + - administrators: set of administrator usernames. + - token_timestamp: dictionary mapping each token to its creation/last confirmation timestamp. + - token_to_username: dictionary mapping each token to its username. + - username_to_tokens: dictionary mapping each username to a set of its tokens. + - token_to_connection_handler: (memory only) dictionary mapping each token to a connection handler + - connection_handler_to_tokens (memory only) dictionary mapping a connection handler to a set of its tokens + """ + __slots__ = ['users', 'administrators', 'token_timestamp', 'token_to_username', 'username_to_tokens', + 'token_to_connection_handler', 'connection_handler_to_tokens'] + model = { + strings.USERS: parsing.DefaultValueType(parsing.DictType(str, parsing.JsonableClassType(User)), {}), + # {username => User} + strings.ADMINISTRATORS: parsing.DefaultValueType(parsing.SequenceType(str, sequence_builder=set), ()), + # {usernames} + strings.TOKEN_TIMESTAMP: parsing.DefaultValueType(parsing.DictType(str, int), {}), + strings.TOKEN_TO_USERNAME: parsing.DefaultValueType(parsing.DictType(str, str), {}), + strings.USERNAME_TO_TOKENS: parsing.DefaultValueType(parsing.DictType(str, parsing.SequenceType(str, set)), {}), + } + + def __init__(self, **kwargs): + self.users = {} + self.administrators = set() + self.token_timestamp = {} + self.token_to_username = {} + self.username_to_tokens = {} + self.token_to_connection_handler = {} + self.connection_handler_to_tokens = {} + super(Users, self).__init__(**kwargs) + + def has_username(self, username): + """ Return True if users have given username. """ + return username in self.users + + def has_user(self, username, password): + """ Return True if users have given username with given password. """ + return username in self.users and self.users[username].is_valid_password(password) + + def has_admin(self, username): + """ Return True if given username is an administrator. """ + return username in self.administrators + + def has_token(self, token): + """ Return True if users have given token. """ + return token in self.token_to_username + + def token_is_alive(self, token): + """ Return True if given token is known and still alive. + A token is alive if elapsed time since last token usage does not exceed token lifetime + (TOKEN_LIFETIME_SECONDS). + """ + if self.has_token(token): + current_time = common.timestamp_microseconds() + elapsed_time_seconds = (current_time - self.token_timestamp[token]) / 1000000 + return elapsed_time_seconds <= TOKEN_LIFETIME_SECONDS + return False + + def relaunch_token(self, token): + """ Update timestamp of given token with current timestamp. """ + if self.has_token(token): + self.token_timestamp[token] = common.timestamp_microseconds() + + def token_is_admin(self, token): + """ Return True if given token is associated to an administrator. """ + return self.has_token(token) and self.has_admin(self.get_name(token)) + + def count_connections(self): + """ Return number of registered connection handlers. """ + return len(self.connection_handler_to_tokens) + + def get_tokens(self, username): + """ Return a sequence of tokens associated to given username. """ + return self.username_to_tokens[username].copy() + + def get_name(self, token): + """ Return username of given token. """ + return self.token_to_username[token] + + def get_connection_handler(self, token): + """ Return connection handler associated to given token, or None if no handler currently associated. """ + return self.token_to_connection_handler.get(token, None) + + def add_admin(self, username): + """ Set given username as administrator. Related user must exists in this Users object. """ + assert username in self.users + self.administrators.add(username) + + def remove_admin(self, username): + """ Remove given username from administrators. """ + if username in self.administrators: + self.administrators.remove(username) + + def create_token(self): + """ Return a new token guaranteed to not exist in this Users object. """ + token = generate_token() + while self.has_token(token): + token = generate_token() + return token + + def add_user(self, username, password_hash): + """ Add a new user with given username and hashed password. + See diplomacy.utils.common.hash_password() for hashing purposes. + """ + user = User(username=username, password_hash=password_hash) + self.users[username] = user + return user + + def remove_user(self, username): + """ Remove user related to given username. """ + user = self.users.pop(username) + self.remove_admin(username) + for token in self.username_to_tokens.pop(user.username): + self.token_timestamp.pop(token) + self.token_to_username.pop(token) + connection_handler = self.token_to_connection_handler.pop(token, None) + if connection_handler: + self.connection_handler_to_tokens[connection_handler].remove(token) + if not self.connection_handler_to_tokens[connection_handler]: + self.connection_handler_to_tokens.pop(connection_handler) + + def remove_connection(self, connection_handler, remove_tokens=True): + """ Remove given connection handler. + Return tokens associated to this connection handler, + or None if connection handler is unknown. + :param connection_handler: connection handler to remove. + :param remove_tokens: if True, tokens related to connection handler are deleted. + :return: either None or a set of tokens. + """ + if connection_handler in self.connection_handler_to_tokens: + tokens = self.connection_handler_to_tokens.pop(connection_handler) + for token in tokens: + self.token_to_connection_handler.pop(token) + if remove_tokens: + self.token_timestamp.pop(token) + user = self.users[self.token_to_username.pop(token)] + self.username_to_tokens[user.username].remove(token) + if not self.username_to_tokens[user.username]: + self.username_to_tokens.pop(user.username) + return tokens + return None + + def connect_user(self, username, connection_handler): + """ Connect given username to given connection handler with a new generated token, and return + token generated. + :param username: username to connect + :param connection_handler: connection handler to link to user + :return: a new token generated for connexion + """ + token = self.create_token() + user = self.users[username] + if connection_handler not in self.connection_handler_to_tokens: + self.connection_handler_to_tokens[connection_handler] = set() + if user.username not in self.username_to_tokens: + self.username_to_tokens[user.username] = set() + self.token_to_username[token] = user.username + self.token_to_connection_handler[token] = connection_handler + self.username_to_tokens[user.username].add(token) + self.connection_handler_to_tokens[connection_handler].add(token) + self.token_timestamp[token] = common.timestamp_microseconds() + return token + + def attach_connection_handler(self, token, connection_handler): + """ Associate given token with given connection handler if token is known. + If there is a previous connection handler associated to given token, it should be the same + as given connection handler, otherwise an error is raised (meaning previous connection handler + was not correctly disconnected from given token. It should be a programming error). + :param token: token + :param connection_handler: connection handler + """ + if self.has_token(token): + previous_connection = self.get_connection_handler(token) + if previous_connection: + assert previous_connection == connection_handler, \ + "A new connection handler cannot be attached to a token always connected to another handler." + else: + LOGGER.warning('Attaching a new connection handler to a token.') + if connection_handler not in self.connection_handler_to_tokens: + self.connection_handler_to_tokens[connection_handler] = set() + self.token_to_connection_handler[token] = connection_handler + self.connection_handler_to_tokens[connection_handler].add(token) + self.token_timestamp[token] = common.timestamp_microseconds() + + def disconnect_token(self, token): + """ Remove given token. """ + self.token_timestamp.pop(token) + user = self.users[self.token_to_username.pop(token)] + self.username_to_tokens[user.username].remove(token) + if not self.username_to_tokens[user.username]: + self.username_to_tokens.pop(user.username) + connection_handler = self.token_to_connection_handler.pop(token, None) + if connection_handler: + self.connection_handler_to_tokens[connection_handler].remove(token) + if not self.connection_handler_to_tokens[connection_handler]: + self.connection_handler_to_tokens.pop(connection_handler) diff --git a/diplomacy/settings.py b/diplomacy/settings.py new file mode 100644 index 0000000..3c36679 --- /dev/null +++ b/diplomacy/settings.py @@ -0,0 +1,24 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Settings + - Provides fixed diplomacy settings shared across project +""" +import os + +DIPLOMACY_ROOT_DIR = os.path.dirname(os.path.realpath(__file__)) +PACKAGE_DIR = DIPLOMACY_ROOT_DIR +TIME_ZONE = 'America/Montreal' diff --git a/diplomacy/tests/__init__.py b/diplomacy/tests/__init__.py new file mode 100644 index 0000000..4f2769f --- /dev/null +++ b/diplomacy/tests/__init__.py @@ -0,0 +1,16 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== diff --git a/diplomacy/tests/network/1.json b/diplomacy/tests/network/1.json new file mode 100644 index 0000000..9727cdc --- /dev/null +++ b/diplomacy/tests/network/1.json @@ -0,0 +1 @@ +{"id":"00128f1d","map":"standard","rules":[],"phases":[{"name":"S1901M","state":{"timestamp":1537459322922097,"zobrist_hash":"6621580922936090403","note":"","name":"S1901M","units":{"AUSTRIA":["A BUD","A VIE","F TRI"],"ENGLAND":["F EDI","F LON","A LVP"],"FRANCE":["F BRE","A MAR","A PAR"],"GERMANY":["F KIE","A BER","A MUN"],"ITALY":["F NAP","A ROM","A VEN"],"RUSSIA":["A WAR","A MOS","F SEV","F STP\/SC"],"TURKEY":["F ANK","A CON","A SMY"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","VIE","TRI"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["KIE","BER","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["WAR","MOS","SEV","STP"],"TURKEY":["ANK","CON","SMY"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD - SER","A VIE - GAL","F TRI - ALB"],"ENGLAND":["F EDI - NWG","F LON - NTH","A LVP - YOR"],"FRANCE":["F BRE - MAO","A MAR S A PAR - BUR","A PAR - BUR"],"GERMANY":["F KIE - HOL","A BER - KIE","A MUN - TYR"],"ITALY":["F NAP - ION","A ROM - VEN","A VEN - TRI"],"RUSSIA":["A WAR - GAL","A MOS - STP","F SEV - RUM","F STP\/SC - BOT"],"TURKEY":["F ANK - CON","A CON - BUL","A SMY H"]},"results":{"A BUD":[],"A VIE":["bounce"],"F TRI":[],"F EDI":[],"F LON":[],"A LVP":[],"F BRE":[],"A MAR":[],"A PAR":[],"F KIE":[],"A BER":[],"A MUN":[],"F NAP":[],"A ROM":[],"A VEN":[],"A WAR":["bounce"],"A MOS":[],"F SEV":[],"F STP\/SC":[],"F ANK":[],"A CON":[],"A SMY":[]},"messages":[{"sender":"ITALY","recipient":"GLOBAL","time_sent":0,"phase":"S1901M","message":"welcome all to game 4! aka potheads and junkies vs nancy reagan and mcgruff =P"},{"sender":"ITALY","recipient":"GLOBAL","time_sent":4,"phase":"S1901M","message":"seriously though, here's to a good game!"},{"sender":"ITALY","recipient":"FRANCE","time_sent":85,"phase":"S1901M","message":"nice to see you again. i think in this game we have a natural inclination to not be fighting from the get-go, don't you? i haven't played with most of these folks before, but i'd say none of them is short on talent."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":223,"phase":"S1901M","message":"hi austria! i don't know about you, but i am a strong proponent of early cooperation between us. i think fighting usually gets us nowhere fast, and usually leads to us both getting killed. what do you think? thoughts on your approach to this game? i haven't played with you or most of the others ever before."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":268,"phase":"S1901M","message":"hi russia! looking forward to the game together. what are you thinking for an opening strategy? maybe take out turkey and then divide up austria?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":328,"phase":"S1901M","message":"hi germany - not sure how you want to approach this game, but i hope us central powers can stick together. i've suggested as much to austria as well. let me know what your thoughts are."},{"sender":"ITALY","recipient":"TURKEY","time_sent":440,"phase":"S1901M","message":"hi turkey - how are things? what are your thoughts for this game? a neutral opening? bouncing russia at black? maybe a full on anti-russian opening to armenia as well?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":3603,"phase":"S1901M","message":"Stay out of Tyrolia, and we're good."},{"sender":"GERMANY","recipient":"GLOBAL","time_sent":3632,"phase":"S1901M","message":"All friends of Germany please respond to me privately. Good luck to some."},{"sender":"GERMANY","recipient":"ITALY","time_sent":3744,"phase":"S1901M","message":"I need a working ally versus France and England, as opposed to someone who will simply ignore the theatre."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":5511,"phase":"S1901M","message":"I am a long term friend of Germany and feel out two countries have a lot to gain from keeping up such relations"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5550,"phase":"S1901M","message":"Greetings northern neighbor, how is the weather?"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":5647,"phase":"S1901M","message":"Let's get specific. I bear an enormous grudge with France, and wish to grind his bones into a fine paste."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":5690,"phase":"S1901M","message":"I've recently been exposed to a new form of alliances in which France Germany and England work together if you are familiar or interested let me know your view on such an arrangement. Or if you have more insightful plans I'm very interested in hearing them."},{"sender":"GERMANY","recipient":"FRANCE","time_sent":5768,"phase":"S1901M","message":"Are you planning a holiday? If so, what are the time details?"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":5805,"phase":"S1901M","message":"lol wow, that is aggressive ^_^ If that is a situation that has mutual benefits I'm very willing to fancy that notion."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5838,"phase":"S1901M","message":"well, which are you looking for? are you planning to take them both on at once?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5917,"phase":"S1901M","message":"Ah, my first game with the lovely and infamous Ms. Bent. How do you do?"},{"sender":"ENGLAND","recipient":"AUSTRIA","time_sent":5997,"phase":"S1901M","message":"Good game to you sir."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":6042,"phase":"S1901M","message":"Eng-Ger is, traditionally, a formidable alliance. I am more than happy to work toward a 2-way draw with you. On the other hand, if you allow Chrisp to get too strong, then he will stab you at the first opportunity, and go for the solo."},{"sender":"ENGLAND","recipient":"TURKEY","time_sent":6150,"phase":"S1901M","message":"Good game to you sir. I wish to keep up communications, you never know where a game is going to head and there may be ways in which we can bend wills to better suit mutual gains."},{"sender":"ENGLAND","recipient":"AUSTRIA","time_sent":6160,"phase":"S1901M","message":"I wish to keep up communications, you never know where a game is going to head and there may be ways in which we can bend wills to better suit mutual gains."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6184,"phase":"S1901M","message":"I would prefer to ally with Eng. I find Chrisp to be untrustworthy and unreliable(too many nmr's). I know that you need to secure a build in year one, but I am hoping that you will see the opportunity to expand west in year 2 and beyond."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6224,"phase":"S1901M","message":"i am completely open to that. let's see how year one shapes up."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6232,"phase":"S1901M","message":"France will be tied up with me, leaving you easy pickings in Iberia."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6247,"phase":"S1901M","message":"OK. Sounds good to me."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6347,"phase":"S1901M","message":"hello england! things are good here. we are enjoying a lovely bowl of pasta e fagioli soup here in our italian homeland, looking forward to expansion into the mediterranean. how are things in buckingham palace?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":6382,"phase":"S1901M","message":"it seems like a sound plan. how are things going with england and russia? i've only heard from you so far."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6486,"phase":"S1901M","message":"I'm chatting with England, but he\/she is giving nothing away. I have not heard from Russia either."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":6548,"phase":"S1901M","message":"Italy may join us versus France in 1902, after she gets a build......"},{"sender":"ITALY","recipient":"GERMANY","time_sent":6612,"phase":"S1901M","message":"yeah just got a generic hello from england after i sent that. let me know if you hear anything of interest from russia and i'll do the same for you."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6679,"phase":"S1901M","message":"Fair enough. Good luck in 1901."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6920,"phase":"S1901M","message":"same back atcha. also, let's both keep tabs on the austrian- i haven't heard anything from him yet."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":8052,"phase":"S1901M","message":"If we work together we wont need the support it it is always appreciated. Also, by 1902 it'll make her look like a vulture lol"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":8095,"phase":"S1901M","message":"Seas are calm and beckon us forth. <br \/><br \/> I wish to keep up communications, you never know where a game is going to head and there may be ways in which we can bend wills to better suit mutual gains."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":8209,"phase":"S1901M","message":"absolutely. for example - you might want to work with france against germany first, and then once things settle down over here a bit, i can help you stab france."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":8251,"phase":"S1901M","message":"i'm also really interested in what you hear from others about their plans. if i hear something that i think might be of use to you, i'll let you know, and i hope you would do the same?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":8349,"phase":"S1901M","message":"So your example was a suggestion? :P<br \/><br \/>I'll put it forth via the council and see where it ends up."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":8450,"phase":"S1901M","message":"well, i could say the opposite as well, but in reality i couldn't help you much with germany afterward. still, i could help you and germany take down france, but then i would expect a decent foothold in france so that i could help you out against G."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":8687,"phase":"S1901M","message":"After some correspondence with Italy I am under the impression that their intentions are not so. It was subtle but I sense that France is more a potential ally to Italy and will be alerted following this allegiance. <br \/><br \/>After much council with Italy, the recommendation was with me siding with France to take yourself out and Italy will help me \"stab\" France. It felt obvious that Italy has hopes of high France relations and this is the best chance for it's survival with a sharp stab from France.<br \/><br \/>This early in the game people are all over the place so keep up intel and let me know where you believe it is going."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":8800,"phase":"S1901M","message":"Ah, I understand that. It is still early in the game so it can go in any direction. It might take awhile before actions have repercussions."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":8946,"phase":"S1901M","message":"Italy is putting it under consideration but knows much wont be gained on her part."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":8960,"phase":"S1901M","message":"absolutely. but you need to make a critical decision about your opening, right? north or south?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":20808,"phase":"S1901M","message":"Yes indeed. I think a DMZ at Pie, GoL, and WMed is sensible, do you? I'll let you know if I ever want to build a fleet in Marseilles if you let me know if you are ever going into Tyrr.<br \/><br \/>I'm interested in knowing what you have arranged with Austria."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":20885,"phase":"S1901M","message":"A Triple alliance? I'd be down if Germany is down. How do you feel about an alliance against Germany though?"},{"sender":"FRANCE","recipient":"GERMANY","time_sent":20956,"phase":"S1901M","message":"I was thinking of visiting London and Ireland around next year. We could get some good travel deals if you want to split the cost with me."},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":21097,"phase":"S1901M","message":"Hey there. =)<br \/><br \/>Any chance you'll open northward?"},{"sender":"FRANCE","recipient":"TURKEY","time_sent":21128,"phase":"S1901M","message":"Hey, if you can keep me updated on the goings on in your corner of the world, I can keep you up to date from my side too. How does that sound?"},{"sender":"FRANCE","recipient":"AUSTRIA","time_sent":21185,"phase":"S1901M","message":"Howdy. =)<br \/><br \/>I'll let you know if I hear any information that might be relevant to you if you'll keep an open ear for the same for me. How does that sound?"},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":22228,"phase":"S1901M","message":"Hey Chris....good luck. We are in a good position to work together.<br \/><br \/>There is a chance, but it depends on Germany. I am just now getting a chance to write and to reply."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":22580,"phase":"S1901M","message":"I will know more about the weather come autumn =)<br \/><br \/>I am sure you are wanting assurances about Norway, just as I am wanting some assurance about Sweden. I haven't played much against Mapleleaf....only one time, I think. But I seems to remember from the forum that he always keeps the Russians out of Sweden. So, I am keeping my eyes open for any friendly nations that can benefit both nations. Let me know how negotiations go with your neighbors. I suspect you will be negiotiating The English Channel with France and the possibility of Belgium with Germany. Good Luck, man."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":23023,"phase":"S1901M","message":"Hello. Saw your post earlier, but I was at work, so I like to respond when I get home. I enjoy your posts on the forum. I was quite glad to see that we had the first game of this tournament together.<br \/><br \/>I think alot of my strategy is going to depend on Germany. I sort of prefer to take out Austria first and then attack Turkey. I am currently in a game as Russia where that is the case...Italy and I just completed our first year as openly allied. Took Turkey by surprise. Its easier to sell because Turkey is usually looking for a Juggernaut, course the better alliance is the one with the Italian. Course if Turkey breaks a Black Sea agreement, then it becomes a concern.<br \/><br \/>Good news is that France asked if I would open North, so it appears he may be going against England....which is a little bit surprising, because you always have to be wary of Chrispminis as the Italians when he is France.<br \/><br \/>Anyway, I am rambling....bottom line....I want you as my primary ally. And lets see what the autumn brings us."},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":23946,"phase":"S1901M","message":"Alright, keep me posted."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":24975,"phase":"S1901M","message":"Right now I'm hoping to gain Belgium so that us three can have our 2 SC's and we'll see what direction thing are heating up soon based on initial moves. I'm moving very passively in hopes that the seas stay calm as long as possible.<br \/><br \/>I will take your suggestion in deep consideration."},{"sender":"TURKEY","recipient":"ENGLAND","time_sent":25335,"phase":"S1901M","message":"Of course. I would very much be willing to participate in an information sharing agreement. My primary concerns are Austria and Russia. Anything you hear from them about possible attacks on me would be appreciated."},{"sender":"TURKEY","recipient":"FRANCE","time_sent":25482,"phase":"S1901M","message":"I am very interested in such an arrangement.<br \/><br \/>The Russian and the Austrian are the primary concerns at the moment, but Italy can quickly become one. Information on those players would be helpful."},{"sender":"TURKEY","recipient":"ITALY","time_sent":25516,"phase":"S1901M","message":"Greetings. My thoughts are very simple - try to win!"},{"sender":"TURKEY","recipient":"ITALY","time_sent":25598,"phase":"S1901M","message":"But in all seriousness, I'm obviously the low rung on the totem pole this game. I do not have any concrete plans at the moment, but anything you might be willing to offer in terms of intelligence would be appreciated."},{"sender":"TURKEY","recipient":"ITALY","time_sent":26079,"phase":"S1901M","message":"My primary concern with an aggressive opening towards Russia is that it leaves you and Austria quite the opening to destroy me. Not ideal.<br \/><br \/>It is tempting (and in my experience, it has been very successful), but I'm not really sure if that is the best option in this game."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":26171,"phase":"S1901M","message":"Greetings and good luck!<br \/><br \/>I would like to find a way for us to work together. We have the obvious common threats of Italy and Russia. Frankly, I don't even have an inkling who to trust right now and I do not have any strategies in mind yet.<br \/><br \/>Let me know if there is something you'd like to see happen."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":26243,"phase":"S1901M","message":"Greetings.<br \/><br \/>I would like to see peace between our nations. Is this something you'd be willing to discuss?"},{"sender":"ENGLAND","recipient":"TURKEY","time_sent":26245,"phase":"S1901M","message":"Russia does seem to be worried about Germany due to his history as Germany so I'm sure he wont be too aggressive to his South for now at least."},{"sender":"ENGLAND","recipient":"AUSTRIA","time_sent":26456,"phase":"S1901M","message":"Okay, I see that as a declaration of war my good man."},{"sender":"TURKEY","recipient":"GERMANY","time_sent":26680,"phase":"S1901M","message":"Greetings.<br \/><br \/>While I have no urgent matters to discuss with you, I wanted to make you aware that I am willing to negotiate with you when the time comes."},{"sender":"TURKEY","recipient":"ENGLAND","time_sent":26747,"phase":"S1901M","message":"That is good news. I would also watch out for Germany, he is very shrewd."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":28350,"phase":"S1901M","message":"Absolutely. I actually would like to take it a step further....and make an alliance for our mutual benefit. I would like to try the Juggernaut, if you are up for it. I would like to DMZ the Black Sea. It would give you a chance to gain control of the Mediterranean and I can focus my ground fleets on Austria\/Germany. Just some of my thoughts"},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":29115,"phase":"S1901M","message":"Hello. How are negotiations with England going? I figure that is probably the key whether you will oppose me in Sweden. If you are needing an ally to assist you in the North Sea, I can assist with that. It wouldn't be too difficult to slide my fleet over to Norway and support you into the North Sea and then we DMZ SWE and DEN. I propose that KIE go to HOL on the open and we work together to contain the north. Frankly, I often see English armies end up in STP more often than I care, now that I the Russians."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":32306,"phase":"S1901M","message":"I would welcome such an arrangement. My discussions with others have indicated that the German is no friend of yours. I would be very careful in dealing with him, he's an accomplished liar, though, I don't really need to tell you that, do I?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":32526,"phase":"S1901M","message":"Haha..... I am familiar with German's reputation, moreso than having actually played with him. But thanks for the heads up."},{"sender":"ENGLAND","recipient":"TURKEY","time_sent":33509,"phase":"S1901M","message":"I'm enjoying the style of every player in this game so far."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":33690,"phase":"S1901M","message":"Since you have an alliance with someone other than me. I'm very interested in who it is lol ^_^<br \/>The playing style of everyone has been enjoyable to me and the first move hasn't even occurred. I have noticed however that most of the people here are not as talkative as I'm accustomed to but it all works.<br \/><br \/>What is your view on anything outside of the game? (I talk too much if that's okay with you ^_^)"},{"sender":"ENGLAND","recipient":"AUSTRIA","time_sent":34420,"phase":"S1901M","message":"lol, you're boring."},{"sender":"TURKEY","recipient":"ENGLAND","time_sent":36955,"phase":"S1901M","message":"I'm not quite sure what you're trying to get at there. :)"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":39737,"phase":"S1901M","message":"that's the nice thing about italy, you don't have to make any real commitments the first year. neither does england really in a way, but going to nwg vs EC does set you up for specific attacks the following year. how are you looking with france? and germany?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":39776,"phase":"S1901M","message":"this turn, i'll focus on getting tunis, and learning as much as possible about what's going on around me. i hope to keep good relations with everyone, and see how the board develops."},{"sender":"ITALY","recipient":"FRANCE","time_sent":39819,"phase":"S1901M","message":"that all sounds good to me. how are things looking from over there? are you getting a preference for working with germany or england?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":39848,"phase":"S1901M","message":"i haven't heard anything from austria. my main focus this year will be on getting tunis and seeing how the board develops."},{"sender":"ITALY","recipient":"TURKEY","time_sent":39991,"phase":"S1901M","message":"not sure about the low rung - i've been watching your excellent performance in the leagues. i think you're in good company and it will be a great game. <br \/><br \/>as for an opening - if you move on russia, obviously that gives me incentive to take a bite out of austria. i haven't heard back from him yet, so i can't say what might develop, but would you be open to that? it probably wouldn't be hard to get russia to commit against austria, and then we will both have a leg up."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":40041,"phase":"S1901M","message":"that all sounds good to me. i was also pleased to be in a game with you! =)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":40149,"phase":"S1901M","message":"here's my thinking on austria vs turkey for the first attack - if it's turkey, i feel like it's easier to then DMZ that area while we move back on austria, whereas if it's austria, we also need to keep a frontier there against germany (who's more likely to intervene also) and then i especially have a bit farther to go to get my units to the front. so, i prefer turkey. i also naturally worry a bit about a jugg if we target austria-hungary first, but i think i could be convinced."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":40202,"phase":"S1901M","message":"great news about france, and it confirms what i've heard. i've only payed with chrisp once before, and i don't know how he views me, but it seems like germany has a strongly shaped opinion of him, so much so that he is biased against him. so, we may see G and E going against F. but i'm not sure."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":40816,"phase":"S1901M","message":"E\/G is bad news for me. I was hoping to avoid a northern confrontation....but it is probably inevitable. I need to read up some on the best way to meet this challenge. Probably another reason, though to go for Austria....to take away any Anschluss that may exist if we let it get too far."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":40981,"phase":"S1901M","message":"Everything that I am hearing says that Germany will oppose me. I am hearing of an E\/G alliance. Perhaps this is just for my benefit....but if it is so, then it will affect both of us."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":41271,"phase":"S1901M","message":"Hello. Good Luck to you. How would you like to proceed? Would you prefer to bounce Galicia or DMZ it?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":41437,"phase":"S1901M","message":"well, the good news about that is that it means that there will be a lot of instability in the west so you shouldn't have to worry about it - F wants to attack E, but G wants to attack F. i don't know what E's preference is..."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":43514,"phase":"S1901M","message":"They both know full well that neither will have good relations or they are both doing a good job of distracting me to the opposite. I do plan on going to Norwegian Sea because I have no reason to commit to a full attack on France, especially before the first turn."},{"sender":"ENGLAND","recipient":"TURKEY","time_sent":43594,"phase":"S1901M","message":"Well other than the fact that no one else seems to talk their ass off as much as me in the beginning lol their \"suggestions\" vary very greatly."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":43658,"phase":"S1901M","message":"Since it hasn't been stated I may as well, EC as a DMZ? We're both well aware that it means war on either end but it feels like a tradition to some degree."},{"sender":"AUSTRIA","recipient":"ENGLAND","time_sent":65770,"phase":"S1901M","message":"No, I've been working. I am in HK here, so I have a very different time zone..."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":65815,"phase":"S1901M","message":"Well, an Austrian\/Turkish alliance would go a long way to making me happy. And, I would throw in the promise to not help Italy with any form of Lepanto."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":65872,"phase":"S1901M","message":"I would prefer to DMZ, but can you be trusted. $64m..."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":65995,"phase":"S1901M","message":"A plea to all the good peoples of the world. We have an uprising in the Austro-Hungarian Empire that threatens to turn very nasty indeed. It would be most appreciated if you could refrain from taking advantage of our weakness and also provide supplies, such as food and medicines... We are willing to come to collect any such supplies that have been donated through your generosity..."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":66069,"phase":"S1901M","message":"Long time no see... I wonder if we can pull a decent alliance out of the bag here. Fancy a crack at Russia? Unusual but definitely workable. I might be able to get Turkey onside. It would be marvelous for all three of us if our Russian neighbour were reduced early on."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":69362,"phase":"S1901M","message":"yeah i think they are at each others' throats. some pretty bad blood there. joining with france against germany might not be a bad idea at all, then turning around and stabbing france."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":70655,"phase":"S1901M","message":"Hello indeed. I would very much prefer to avoid early fighting as I obviously have some hungry neighbours to fend off. What would you prefer here. NAP between Treiste and Venice? DMZ for Tyrolia? Help into Turkish lands?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":70669,"phase":"S1901M","message":"How experienced are you?"},{"sender":"ENGLAND","recipient":"AUSTRIA","time_sent":71157,"phase":"S1901M","message":"Every time I've been on, you were on so how is that an excuse?"},{"sender":"AUSTRIA","recipient":"ENGLAND","time_sent":71278,"phase":"S1901M","message":"I have looked in to deal with e-mails and check messages for games that have urgent deadlines. I left this one as I knew I had quite a lot of time. I have also been out earning money."},{"sender":"AUSTRIA","recipient":"ENGLAND","time_sent":71298,"phase":"S1901M","message":"That last line makes me sound like a pimp or a hooker."},{"sender":"TURKEY","recipient":"ENGLAND","time_sent":71512,"phase":"S1901M","message":"Fair enough."},{"sender":"ENGLAND","recipient":"AUSTRIA","time_sent":72040,"phase":"S1901M","message":"lol. Are you insinuating I do go out and earn money? Sometimes I feels like I'm the only one who finds it absurd that so many people can play this game at work."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":72184,"phase":"S1901M","message":"From my discussions with Russia, he may not be a threat to either of us in the short term, which is potentially of benefit to both of us."},{"sender":"TURKEY","recipient":"ITALY","time_sent":72295,"phase":"S1901M","message":"Thanks for the kind words. I'm still trying to get a feel for the other players, but I will communicate anything of note."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":72460,"phase":"S1901M","message":"Under an alliance, what sort of movement on my part would you like to see?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":74467,"phase":"S1901M","message":"let's keep Tyr DMZ and can we leave tri and ven empty?<br \/><br \/>or we could try a key lepanto. i know it's risky for you, but if you look at my past history with italy, i like to do it and not stab austria-hungary. if you prefer not to, that's also fine."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":74492,"phase":"S1901M","message":"as for experience, i guess i don't really know the answer. i've played the game a bit. why do you ask?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":74547,"phase":"S1901M","message":"finally heard from austria, but not much. i'll keep you posted as things develop. any word from russia? i got a long post from him to begin with suggesting i help him attack austria, but haven't heard anything since."},{"sender":"ITALY","recipient":"GERMANY","time_sent":74580,"phase":"S1901M","message":"alright i heard from austria, he seemed friendly. have you communicated with him? how did it go?"},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":74819,"phase":"S1901M","message":"Army to Bulgaria and also to Const so that you can follow across. We need strength to take the Balkans efficiently."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":75217,"phase":"S1901M","message":"...Because I needed to know if you could play something like a Lepanto. There is an interesting variation called Three Fleets. It is the best way for Italy to attack... France.<br \/><br \/>You move Venice to Trieste first move, with me running for Albania (fleet) and Serbia (army). The Austrian moves look typical and the Italian a stab. But, second move, I hit Trieste with two units. The clever bit is that you get to disband instead of retreat and, build a fleet instead. You also get Tunis and another fleet, giving three fleets (hence the name) and enough to overwhelm the Frogs right from the start.<br \/><br \/>I take the first big risk - like the Key Lepanto; but you take the risk when you attack France, at least until you get the next build. It is reasonably fair in that respect.<br \/><br \/>I have wanted to do this one for some time and am more than game to try. It would certainly turn things around and we could well get a great start for the League. We should be getting different countries each game, except for Game Eight where I believe we get the same country as in Game Two. Now, think about it, unless you get Austria (!!) and I get Italy (!) in Game Two, a decent start here (safe draw) would be very advantageous as we will get better country allocations most of the time."},{"sender":"AUSTRIA","recipient":"ENGLAND","time_sent":75291,"phase":"S1901M","message":"I don't play at work, I play between work. Fortunately, I am my own boss, so it is just a matter of finding time, not permission."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":75376,"phase":"S1901M","message":"i am familiar with three fleets and three armies."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":75484,"phase":"S1901M","message":"and i personally love to play italy and prefer to austria to both turkey and france. but anyway, i'm game. i've done it once before. or maybe we did three armies, i can't remember. anyway, it's an interesting proposition."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":75522,"phase":"S1901M","message":"so if we go with that, what would your next steps be?would you move on turkey or russia. i'm pretty sure russia is gunning for you from the start, so keep on eye on that one."},{"sender":"ENGLAND","recipient":"AUSTRIA","time_sent":75525,"phase":"S1901M","message":"I didn't say you did, it was more of a I'm glad you dont because so many other people do. You stated you weren't playing because of work\/responsibilities."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":75586,"phase":"S1901M","message":"the nice thing about three fleets and going after france first is that it gives us a better chance of breaking out of the med further down the line and not getting stymied by any E\/F cooperation once we make some gains."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":75672,"phase":"S1901M","message":"If you want to go for it, I will certainly do my part. I am a very reliable ally and like to se decent plans taken to their conclusion. I do not cut and run for quick gains.<br \/><br \/>The opening gives me Serbia and, maybe, Greece - best case. You get the one build but are ready to assault France. I will have to garrison the north to keep Venice safe.<br \/><br \/>We would need to be very quiet as we do not want to see a German army in Tyrolia first move. <br \/><br \/>If you say yes to Three Fleets, I will go for it."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":75765,"phase":"S1901M","message":"I am trying to cooperate with Turkey. I would like to see him go north and I can expand via the Balkans with him. The break out from the Med is the bonus for this opening. Incidentally, I have more wins with Italy than any other country: I like playing italy as well."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":75874,"phase":"S1901M","message":"i like it. i'm pretty sure we are safe with germany not going to tyrolia - the hard part will be when i stab you if he offers support against me. but maybe he will be busy with france himself, he seems obsessed with attacking F."},{"sender":"AUSTRIA","recipient":"ENGLAND","time_sent":75883,"phase":"S1901M","message":"Ah, then I applaud you.<br \/><br \/>I knew a lawyer in London who was always turning up on a Football website (soccer) - he boasted that he was posting while sitting opposite clients in meetings. They thought he was recording details for their cases\/claims. He got reported by another user of the site - someone contacted his employers and he got the boot. Justice at last where a lawyer was concerned."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":75990,"phase":"S1901M","message":"The Three Fleets is one of the better disguised openings, especially if you push Rome to Tus or Venice first move because that says 'no Lepanto', and hence, \"no alliance with Austria\"."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":76029,"phase":"S1901M","message":"Welcome to Trieste then. The food is good and the shops accept all hard currencies."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":76061,"phase":"S1901M","message":"well, i often use and see rom-ven as part of a fake-out during a lepanto, so i'm not so sure about that. but it still seems like a plan to me. i'm up for it."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":76517,"phase":"S1901M","message":"alright, i'll order ven-tri, rom-ven and nap-tun"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":76537,"phase":"S1901M","message":"Good. That is very attractive as I want to try something interesting in the first game and with a weaker country. No point in playing a common opening and getting removed without hope."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":76599,"phase":"S1901M","message":"haha sure thing. let me know what you are hearing from G - and also from F\/E. i think we need to get E\/F to attack G so france builds armies, makes it easier to break out into the atlantic, no?"},{"sender":"ENGLAND","recipient":"AUSTRIA","time_sent":76682,"phase":"S1901M","message":"I'd say that it was absurd that someone went that much out of their way but I like they must have, have met too many unethical lawyers and you can't go anywhere without a strong business and\/or personal ethic."},{"sender":"AUSTRIA","recipient":"ENGLAND","time_sent":76869,"phase":"S1901M","message":"The person who contacted the employer had been savaged on the Forum by said Lawyer. A matter of sweet revenge."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":76940,"phase":"S1901M","message":"I have played with germany before, and it was a good game. I am still waiting for him to turn up and talk but expect him to do so. I will be very careful in what I say but will encourage him to look north or east."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":77759,"phase":"S1901M","message":"I will not enter any orders until I get to talk with Germany. I want him to think that I am working with him. Which I might if he decides to move against Russia. The Three Fleets is on though, have no fear."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":77803,"phase":"S1901M","message":"Where are you?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":77951,"phase":"S1901M","message":"alright, that's fine. i went NMR in a couple games over the weekend when life overwhelmed and prevented me from getting online, so i'm trying to do a better job of entering orders from the get-go."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":78053,"phase":"S1901M","message":"I am with you on a stab of France, both Italy and France keep suggesting I attack you then Italy adds that she'd assist me in stabbing France but after analyzing their history it is much more beneficial that we take out France while I deal with Italy and assist you in Russia who statistically (based on his own Russia attempts) is weak in that positioning."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":78231,"phase":"S1901M","message":"Well not a stab per se but an all out war."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":78302,"phase":"S1901M","message":"I am pretty much able to get on line every day and at multiple times during the day so I have no worries here. In fact, I have never gone CD, and I do not think I have NMR'd either. Always a first time!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":78412,"phase":"S1901M","message":"yeah. i generally am able to as well, but at work i just sit in front of the computer all day and on weekends i only go online once in a while. a weekend of back to back dj gigs and staying up all night meant that i missed a couple deadlines =("},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":78458,"phase":"S1901M","message":"The deadlines for this tournament are very comfortable so we should be okay."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":78667,"phase":"S1901M","message":"you're right."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":81543,"phase":"S1901M","message":"In a chair. <br \/><br \/>I suspect that I will be occupied with one, or all, of F-E-I. <br \/><br \/>On the positive side, I bear you no malice."},{"sender":"GERMANY","recipient":"TURKEY","time_sent":81576,"phase":"S1901M","message":"Thank you. I wish you good fortune."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":81662,"phase":"S1901M","message":"I suspect everybody, of course. It is good to read your message. <br \/><br \/>I am open to negotiations. Let's see how term one shapes up."},{"sender":"GERMANY","recipient":"FRANCE","time_sent":82019,"phase":"S1901M","message":"We are visiting London and Paris next year summer-ish. Have fun!!!"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":84176,"phase":"S1901M","message":"Thank you for the info."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":84433,"phase":"S1901M","message":"Not very talkative now are you? lol"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":84694,"phase":"S1901M","message":"I'm a man.<br \/><br \/>Sorry."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":84802,"phase":"S1901M","message":"I agree with everything that you have communicated. A and R want to be cool. Italy professes the same(as I've told you). T says hello. F has shared his vacation plans with me.<br \/>Happy now?"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":84965,"phase":"S1901M","message":"I'm a man as well, that in no way entails lack of communication especially in a game such as this."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":85225,"phase":"S1901M","message":"After analyzing most people's playing style I've come to the conclusion that in the long run you are the best bet for mutual gain. Chrisp has only done well as France when he attacked England so I can't allow him to get too strong and will utilize mapleleaf's aggression to do so. I'll be glad to share France with you and as soon as that worry is quelled I intend on teaching Germany the meaning of Diplomacy. I know that wont gain you much other than the share of France but where do you feel you'd benefit more in my short term equation of our aggressive neighbors?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":85396,"phase":"S1901M","message":"interesting. well, i was thinking to move on france after the first couple turns anyway, so i wonder if you'd want to hold off and try to work with france against germany until i can get in position, and then we can hit france hard. but if you got for france from the start, i can't really complain!"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":85450,"phase":"S1901M","message":"it's funny, i used to always look at players' game histories, but i barely ever do now. not sure why that changed. maybe i will have a look for this game. i'm curious as to what you learned about me?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":85698,"phase":"S1901M","message":"Other than the fact that you are only okay with Italy? Not much lol Haven't played with you and you don't talk as much as I make people talk. Half the people tend to be non talkers which is a weird concept to use in Diplomacy but that's okay. My focus was on my 3 neighbors, Russia has a very poor history playing that country, like I said France always went for England, and Germany is all over the place and his lack of effective communication and aggression toward France I'm just using to benefit against France and encourage Russia I'll assist with Germany since I'd think he would be wary of England from being stabbed so much same with Germany a more confrontational alliance might sway him in a direction that might benefit me."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":85798,"phase":"S1901M","message":"*Disclaimer: One can make the comment that talking too much is bad in Diplomacy but the more aware you are in the degrees in which it becomes harmful the better you can utilize it in an ulterior motive that is much more effective than not talking could ever be."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":85965,"phase":"S1901M","message":"sorry? i didn't follow most of that. maybe slow down and make proper sentences? =)<br \/><br \/>i talk a lot, just not too much to say right now. i've not heard much from most people on the board, aside from you, so i can't really speculate about other peoples' plans. once i hear a bit more, i'll have more to say."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":86045,"phase":"S1901M","message":"also how can you say you talk so much? you only have 2000 game messages. that's very little.<br \/><br \/>i have almost ten times as many game messages, and only a few more games than you."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":86061,"phase":"S1901M","message":"so you talk about how much you talk, but you don't actually talk much ;D"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":86100,"phase":"S1901M","message":"now, for the item at hand. i think getting france to commit against germany shouldn't be hard. plus, if you work with germany, he's going to try to find excuses to build fleets. whereas if you work against him, he won't be able to build anything."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":86153,"phase":"S1901M","message":"france, on the other hand, will only build armies if he's going against G. that's a good thing for both of us. once you have gotten deep into germany, i swing up and help take on france, while you get G to help you against france in exchange for survival."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":86158,"phase":"S1901M","message":"No actually, half of my games were draws and a few SC's take overs. Also, a lot of people don't respond back so I wouldn't have anything further to say. I'll assume you're a woman which makes them more inclined to talk back :P"},{"sender":"ITALY","recipient":"GERMANY","time_sent":86188,"phase":"S1901M","message":"it sounds like england is interested in working with you against france. have you heard the same from him?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":86216,"phase":"S1901M","message":"oh i talk to people even if they don't talk. you still have to chat them up!!"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":86251,"phase":"S1901M","message":"My first 2 moves are neutral to both, I expect to find aggression towards me during my 2 moves and that will ensure the direction I head."},{"sender":"ITALY","recipient":"FRANCE","time_sent":86269,"phase":"S1901M","message":"heads up that E and G are talking. G seems to have some irrational drive to move against you, that i can't figure out, but E doesn't really seem to care one way or the other. i don't want to see you go down quickly, so you may want to see if you can work things out with E."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":86292,"phase":"S1901M","message":"I send a message to every country in the game, my rate is 50% no responses. I think I'm to flamboyant via text for em."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":86297,"phase":"S1901M","message":"alright, that sounds like a sensible opening style. are you going to get one of them to help you take belgium? that would help you a lot."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":86362,"phase":"S1901M","message":"I just looked at your game messages.. and damn woman lol"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":86419,"phase":"S1901M","message":"I suggested to both of them that Belgium would be appreciated."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":86430,"phase":"S1901M","message":"you've only heard from 3 out of the other six? that's a shame. yeah, not a whole lot of press so far in the game, but the turns are long, so there's still a lot of time. be careful not to make up your mind just yet."},{"sender":"ITALY","recipient":"GERMANY","time_sent":86457,"phase":"S1901M","message":"i also have heard from austria-hungary and he's very keen on us central powers getting along, so that's great news i'd say."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":86516,"phase":"S1901M","message":"I'm aware, I reassess with every new bit of intel."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":86634,"phase":"S1901M","message":"sounds good."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":86685,"phase":"S1901M","message":"i think i'll give it a go then and take tri. i've got austria to open south and leave it empty."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":86694,"phase":"S1901M","message":"are you going to move into gal?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":86744,"phase":"S1901M","message":"what's the good word? any updates? let me know how things are coming along with russia - if you'd commit to moving on him, i would go for austria. russia is chomping at the bit to move on austria anyway, so we can take advantage of that."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":86987,"phase":"S1901M","message":"I will ....if we are agreed to take out Austria first, then ?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":87049,"phase":"S1901M","message":"it would be rather dumb of me to take tri and then not finish the job."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":89430,"phase":"S1901M","message":"LOL ..... I certainly didn't mean to imply you were dumb. Just checking on the order of attack ;)<br \/><br \/>I will hit GAL."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":90195,"phase":"S1901M","message":"haha i was just kidding with you =)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":91248,"phase":"S1901M","message":"but that sounds great."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":91344,"phase":"S1901M","message":"Yes, that's fine with me."},{"sender":"FRANCE","recipient":"TURKEY","time_sent":91417,"phase":"S1901M","message":"Well Italy is seeking peace with me, so if she isn't going after Austria, you might have reason to worry. Perhaps you can figure out what Germany is up to?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":91489,"phase":"S1901M","message":"From two other countries I'm hearing rumours of an EG alliance. Obviously this is disturbing, and perhaps you could shed some light on the fact? It might just be a rumour, but with nothing else to go on with these first moves, a rumour is all I have."},{"sender":"FRANCE","recipient":"ITALY","time_sent":91547,"phase":"S1901M","message":"Really. I've always had the vibe that Germany had it out for me. What have you heard? Maybe you can put in a good word for me with England."},{"sender":"FRANCE","recipient":"GERMANY","time_sent":91592,"phase":"S1901M","message":"Good to hear. Bounce at Bur?"},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":91682,"phase":"S1901M","message":"Well that's certainly disturbing. Who are you hearing this EG alliance stuff from? I have a guess in mind, and it would be interesting if it were confirmed.<br \/><br \/>Also if you have EG on your hands, I would suggest opening northward, but what have you heard from the south?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":91690,"phase":"S1901M","message":"i already did, but he seems more interested in taking advantage of germany's pre-disposition. personally, i can't stand mapleleaf, but i need peace with germany right now - so i don't mind at all if someone else takes him out =)"},{"sender":"ITALY","recipient":"FRANCE","time_sent":91713,"phase":"S1901M","message":"try offering bel to england, that might get him in the bag."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":92072,"phase":"S1901M","message":"can i ask about your name? what is the meaning of it?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":92086,"phase":"S1901M","message":"what are you hearing from turkey, btw?"},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":92255,"phase":"S1901M","message":"Italy is my source on the E\/G. England hasn't responded to me, so I expect some conflict there. I have offered Germany assistance into the North Sea if I can obtain Scandanavia. We all know that Mapleleaf is sort of a \"maverick\".....so IDK....he isn't really discussing strategy....all I get is a wait and see. So I expect the worse."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":92306,"phase":"S1901M","message":"Also Turkey has told me that Germany means to come after me....this was before Germany and I corresponded, though."},{"sender":"FRANCE","recipient":"ITALY","time_sent":92819,"phase":"S1901M","message":"May I ask how you know that E and G are forming some sort of alliance?"},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":92860,"phase":"S1901M","message":"I had a feeling Italy was the source of your information. She has told the same thing to me. Now I wonder how reliable that information is."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":93459,"phase":"S1901M","message":"Turkey tells me that Germany is coming after me. Confirmed what you had said. I did offer to help Germany gain control of the North Sea once I got Scandanavia....he said he was intrigued and would talk after the turn.....so he blew off, basically. I think I will keep my armies southern rather than trying to force a northern open....just in case Germany crosses the Rhine.<br \/><br \/>Spell of Wheels is a song title from Peter Case who is a former Plimsouls member (punk rock) turned acoustic folk troubadour. The song is about 5-6 kids who have nothing better to do than hit the road and find trouble. But they aren't really prepared for what is out there. A black car chases them.....(urban myth sort of tale)....they see the shotgun in the black car's window and they all pull out their knives...(lol). After the experience, they decide to just be small time criminals. The song also gives a feeling for a longing to be back home.<br \/><br \/>I used the moniker in a \"Mafia\" game that I play....but I thought it kind of applied here....being that I want TO RULE THE WORLD =)"},{"sender":"ITALY","recipient":"FRANCE","time_sent":93523,"phase":"S1901M","message":"from talking to them?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":93943,"phase":"S1901M","message":"i haven't heard anything about germany moving on you. all i've heard is about him attacking france."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":93992,"phase":"S1901M","message":"cool about the name, i'll have to check it out. a punk rocker myself, or at least i used to be =\/"},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":94328,"phase":"S1901M","message":"Well....I suppose we could be looking a central powers play. She should open against Austria on the open....if that happens, then probably no central powers. If she doesn't, then she lied about her open.<br \/><br \/>I suppose you could bounce PAR and MAR in BUR...just to ensure no border crossing on the open. You can still acquire Spain in the fall. Better safe than sorry.<br \/><br \/>What can you tell me about Germany? Are you two discussing going against England? What is he telling you?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":94414,"phase":"S1901M","message":"E\/G=Scandanavia for themselves=STP next."},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":94432,"phase":"S1901M","message":"He's very terse. =\/"},{"sender":"FRANCE","recipient":"ITALY","time_sent":94497,"phase":"S1901M","message":"And both of them told you that they were forming an alliance? I thought they were more tight lipped."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":94548,"phase":"S1901M","message":"Plimsouls are mid\/late 70's. Peter Case recreated himself after leaving the group. Spell of Wheels is very much a folk\/rock song."},{"sender":"ITALY","recipient":"FRANCE","time_sent":98020,"phase":"S1901M","message":"well, what can i say... they were trying to recruit me for the effort."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":100314,"phase":"S1901M","message":"I'm sure you're aware of Germany's stance. Myself? I am neutral until otherwise shown. My first moves are as non aggressive as possible and my only intents are gaining Norway and Belgium as an even divide of SC's.<br \/><br \/>I'm sure you have Italy Austria and Russia with the thoughts of an E-G alliance because it is the easiest way for Germany to accomplish his goal and I'm positive Italy is the foremost runner for wanting to ensure your safety to maximize Italian gain for now."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":100336,"phase":"S1901M","message":"At least this is the views that I have gained from my position and correspondence."},{"sender":"ITALY","recipient":"FRANCE","time_sent":102048,"phase":"S1901M","message":"germany basically told me right off the back, nothing tight lipped there at all. england has been a bit more wisyhy washy, but he indicated he was likely to move against you after the initial moves. so, not exactly an iron-clad alliance, but there you have it. on the other hand, russia seems paranoid about a possible E\/G alliance against him in the north."},{"sender":"TURKEY","recipient":"ITALY","time_sent":103862,"phase":"S1901M","message":"from what I can gather, Russia is wanting to contain Germany."},{"sender":"TURKEY","recipient":"ITALY","time_sent":103882,"phase":"S1901M","message":"So your intel on him seems pretty solid."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":103952,"phase":"S1901M","message":"So your goals would be to keep Italy and Russia from getting Balkan gains?"},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":111731,"phase":"S1901M","message":"I doubt that all three will be after you, but the F\/E alliance is always possible. No chance of an eastern move?"},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":111769,"phase":"S1901M","message":"Yes. Basically. I want to see Russia humbled right from the start."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":111800,"phase":"S1901M","message":"Germany talking at last. Nothing specific yet."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":118452,"phase":"S1901M","message":"Interesting proposal. Could work well."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":118507,"phase":"S1901M","message":"Any thoughts on opening? Anything from Austria?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":119168,"phase":"S1901M","message":"Very few words from Austria. We discussed DMZ'ing GAL....but he pretty much told me that he didn't believe I would follow through....so now I kinda feel like I have to, for fear that he will move there."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":120091,"phase":"S1901M","message":"haha....you too? I guess England was the first to prostrate himself before Mapleleaf, then! Through private messages, of course :)"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":124447,"phase":"S1901M","message":"so it's been a few more hours. anything new? i had a nice long talk with russia about his high school, and am on good terms with AH, T, and F, which is always good. i guess i'm on good terms with everyone!"},{"sender":"ITALY","recipient":"TURKEY","time_sent":124535,"phase":"S1901M","message":"i try! yeah russia doesn't seem too intent on attacking you, so you probably can grab the black sea if you want to. never hurts right? and even with a neutral opening of con-bul and smy-con, you'll still be good."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":124582,"phase":"S1901M","message":"haha got confused between games. that was russia in a different game. russia here and i had a talk about music. =)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":124639,"phase":"S1901M","message":"right on, i'm googling it right now. <br \/><br \/>i'm not yet sure what austria is doing with vienna, let me see what i can find out. has he said anything to you about it?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":124762,"phase":"S1901M","message":"Well, I'm willing to help you with Belgium if you move with me against Germany next year. I get the hunch that he's not really a fan of you either."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":124785,"phase":"S1901M","message":"so wait, i'm a little confused. basically, is the jig up once i disband rather than retreat? or is there some way that we can cover it? b\/c why wouldn't i just retreat to budapest? or vienna, if that army is moving to bud. sorry, want to be clear on it before we do it!"},{"sender":"FRANCE","recipient":"ITALY","time_sent":124801,"phase":"S1901M","message":"Alright. Thanks a lot for that info. That helps me."},{"sender":"FRANCE","recipient":"AUSTRIA","time_sent":124829,"phase":"S1901M","message":"No response? What are you up to?"},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":124888,"phase":"S1901M","message":"Haha, yeah I get that feeling too. I'm trying to work England, but he's being wishy-washy. Can I count on you for some northern support? EG looks more likely than I'd like to admit."},{"sender":"ITALY","recipient":"FRANCE","time_sent":125035,"phase":"S1901M","message":"i do think you have a chance at convincing england to work with you against germany, and that might be worth a try. he said he's going to open north to keep options open, so that's certainly good news for you."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":128201,"phase":"S1901M","message":"No...Austria has communicated little to me."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":128494,"phase":"S1901M","message":"Well, to be fair, I'm sure most of us would feel the same way if we were in his shoes.<br \/><br \/>I'd like to say he's telling the truth, but I get the feeling that you're instinct is right."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":128640,"phase":"S1901M","message":"I am only committing my fleet to the north this year....my real fear is that Germany crosses the Rhine....so I am keeping an army close to home. But whatever I can do in the north. This is really a quandry because I have issues in the East, but feel like I can't ignore Germany.<br \/><br \/>How is your relationship with Austria? Mine is poor with him....but if we had a triple alliance then it would give the 3 of is the best shot of getting to the middle game."},{"sender":"TURKEY","recipient":"ITALY","time_sent":128647,"phase":"S1901M","message":"That is the sense I'm getting as well."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":128744,"phase":"S1901M","message":"3 of US......not \"3 of is\""},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":129294,"phase":"S1901M","message":"Austria hasn't even responded to me yet. =\/<br \/><br \/>Though I've had decent relationships in the past with him. My most recent one was not so peachy though."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":130773,"phase":"S1901M","message":"How about you take Burgandy....I move Silesia and support you into Munich the first year. You can follow up by supporting me into Berlin the next. I will have a fleet in the Baltic most likely by then."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":160170,"phase":"S1901M","message":"Could?<br \/><br \/>How about, should!"},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":160240,"phase":"S1901M","message":"Sorry, been very busy. I am plotting my own downfall it would seem. I cannot get a meaningful alliance sorted. Everyone is giving me the, 'Let's see how the first move goes' line. Well, I can tell them now, the first move is towards me."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":160318,"phase":"S1901M","message":"You could retreat but the whole idea is for you to do the unexpected and disband - that's how you get the extra fleet. As long as I hit you with strength two and you do not support yourself you will have the option to disband."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":160382,"phase":"S1901M","message":"Are we bouncing in Galicia? I think we probably are."},{"sender":"GERMANY","recipient":"FRANCE","time_sent":162527,"phase":"S1901M","message":"Is that what you'd prefer. I'd rather not actually, but I will if necessary."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":162573,"phase":"S1901M","message":"Sorry, no. It would be foolish on my part."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":162734,"phase":"S1901M","message":"Okay... But keep it in mind. perhaps you could at least bounce the Tsar at Sweden?"},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":162763,"phase":"S1901M","message":"Bounce the Tsar, bounce the Tsar... It has a nice ring."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":162819,"phase":"S1901M","message":"I could do many things. <br \/><br \/>I keep everything in mind.<br \/><br \/>Be happy with the non-agg."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":162831,"phase":"S1901M","message":"Grmany seems unwilling to move East, but I am trying to get him to bounce at Sweden. That would at least put the brakes on the Tsar.<br \/><br \/>My own moves are set: Trieste will be wide open. A chance for Italy to show his true worth."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":162875,"phase":"S1901M","message":"Oh, I am. But, as an Austrian, I do have a sense of paranoia..."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":162916,"phase":"S1901M","message":"\"his\"? not so much! lol<br \/><br \/>yes, i understand that the point is for me to disband. what i am saying is that once i disband, won't that make it obvious to the others that we are not actually at war. it doesn't matter, it's just something to take into account."},{"sender":"ITALY","recipient":"TURKEY","time_sent":162943,"phase":"S1901M","message":"and i have decided to take a \"stab\" at austria, so we'll see how that goes."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":162965,"phase":"S1901M","message":"What, no offers of help? What a cruel world. Where is your humanity, where is your charity, where is my hope in time of need. 'I am not so fortunate as you in my friends...' (Theoden, King, Third Age)"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":162968,"phase":"S1901M","message":"what are you doing with vienna? bouncing russia at gal? something else?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":163018,"phase":"S1901M","message":"Are, so the rumours about your gender are true? Well, this could be an interesting duet."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":163032,"phase":"S1901M","message":"i'll send a brigade with supplies from venice to trieste, if you'll let me pass. even a large horse, as a gift of art, to your impoverished people. ;D"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":163073,"phase":"S1901M","message":"The disband is obvious, but at that point you have three fleets for Spring 1902 and France cannot possibly match you down south. Especially not if you have Piedmont too."},{"sender":"GERMANY","recipient":"GLOBAL","time_sent":163085,"phase":"S1901M","message":"We wants it, we needs it. Must have the precious. They stole it from us. Sneaky little hobbitses. Wicked, tricksy, false! (Gollum)"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":163088,"phase":"S1901M","message":"rumors? haha<br \/><br \/>anyway yeah i'm a lady. though some dispute my qualifications for that particular term..."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":163119,"phase":"S1901M","message":"Vienna bouncing at Galicia - it is the one variable that is not completely under control, but he asked for it and I offered it. He should bite."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":163135,"phase":"S1901M","message":"Bouncy, bouncy, bouncy..."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":163217,"phase":"S1901M","message":"I shall be watching Disc 6 tonight in the extended version. Got as far as the Stairs of Cirith Ungol... Shelob next."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":163273,"phase":"S1901M","message":"sounsd good."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":163318,"phase":"S1901M","message":"he told me he agreed with you to bounce at gal? do you want to let him go there without a bounce, it makes it that much harder for him to kick me out."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":163505,"phase":"S1901M","message":"I will stay away from GAL as we prior agreed. I don't know if you have designs on Greece, but I wouldn't be surprised by an open from Italy on Trieste. So if you do go to GAL, then I would hold TRI.<br \/><br \/>I suggested to France that perhaps a triple alliance may be in order between the 3 of us. Are you interested?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":163561,"phase":"S1901M","message":"If you promise to order Mos-StP, then I shall order Kie-Hol."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":163614,"phase":"S1901M","message":"Yes....I am seriously considering not moving there now.....though our agreement was to DMZ. He wrote me this morning to bounce it."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":163806,"phase":"S1901M","message":"so he'd be in alb, ser, and gal. i would be in tri and ven, and you would be...?trying to figure out the best way to put a serious hit on him - a prolonged battle between him and i really doesn't benefit me."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":163827,"phase":"S1901M","message":"Alright....I will promise then to do that."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":163992,"phase":"S1901M","message":"Here's to a fine working relationship(holds breath)."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":167728,"phase":"S1901M","message":"Well, I'm currently homeless :P and can't believe I'm online via stolen wi-fi."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":167759,"phase":"S1901M","message":"Germany doesn't seem to like anyone."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":167833,"phase":"S1901M","message":"why are you homeless, if i can ask?"},{"sender":"FRANCE","recipient":"GERMANY","time_sent":167938,"phase":"S1901M","message":"It is what I'd prefer."},{"sender":"FRANCE","recipient":"AUSTRIA","time_sent":167952,"phase":"S1901M","message":"=\/"},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":167973,"phase":"S1901M","message":"Ok. That sounds good to me."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":168028,"phase":"S1901M","message":"I lost my job 3 months ago and the lease was up and they wouldn't renew it without a stable source of income. I was able to pay all my bills but I've been dried out in terms of expendable savings."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":168099,"phase":"S1901M","message":"shoot, i'm sorry to hear that. it's tough times."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":168701,"phase":"S1901M","message":"Germany has said he would allow me Sweden with a northern open....I am probably falling into a big trap here, but I will do so...that will leave the door open for us to kill England and then work together to kill Germany....unless I fall into a trap :("},{"sender":"ENGLAND","recipient":"ITALY","time_sent":168702,"phase":"S1901M","message":"I can find work out of my state but I can't leave my daughter with her mother, it would be devastating to us both (daughter and myself; I've been a single father for 3 years)"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":168745,"phase":"S1901M","message":"It is a good idea - the triple, but I would prefer to bounce (Galicia) for safety."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":168766,"phase":"S1901M","message":"Besides, it will cover up any hint of us two working together."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":168837,"phase":"S1901M","message":"so sorry to hear that. well maybe you should pass off your games to someone else? or maybe playing a game helps relieve stress of the situation you're in. in any case, i hope things are on the up and up again soon for you."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":168905,"phase":"S1901M","message":"The game is a very good distraction. I always assume it'll turn around, things tend to have a balance that is rewarding in a whole."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":169018,"phase":"S1901M","message":"that's a good attitude."},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":169141,"phase":"S1901M","message":"Well Austria has responded saying he's been really busy and hasn't managed to come out with any alliances, so I don't think you have to worry about him. Turkey might try something.<br \/><br \/>If you cover BLA and Gal then I think you should be safe. I'd rather you open north than move to Sil anyway."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":169342,"phase":"S1901M","message":"I can't go Galicia to bounce you."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":169411,"phase":"S1901M","message":"OK...thanks. Talk with you after the turn then."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":169443,"phase":"S1901M","message":"OK...worked it out. I will bounce you now. Sorry...lots of diplomacy going on at once."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":169701,"phase":"S1901M","message":"Thanks. That is a first move that makes sense for both of us."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":169702,"phase":"S1901M","message":"I have to tell you that I will bounce in GAL this turn. I realize that messes you up a bit....but I had to let you know so that you can properly plan.<br \/><br \/>This has been a very intense diplomatic opening turn."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":169761,"phase":"S1901M","message":"oh it's no problem at all. that's good that it's been intense, makes for a good game, no?"},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":175367,"phase":"S1901M","message":"fair enough!<br \/><br \/>It shall work well!"}]},{"name":"F1901M","state":{"timestamp":1537459322930857,"zobrist_hash":"8252897835863866531","note":"","name":"F1901M","units":{"AUSTRIA":["A VIE","A SER","F ALB"],"ENGLAND":["F NWG","F NTH","A YOR"],"FRANCE":["A MAR","F MAO","A BUR"],"GERMANY":["F HOL","A KIE","A TYR"],"ITALY":["F ION","A VEN","A TRI"],"RUSSIA":["A WAR","A STP","F RUM","F BOT"],"TURKEY":["A SMY","F CON","A BUL"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","VIE","SER","ALB"],"ENGLAND":["EDI","LON","LVP","NWG","NTH","YOR"],"FRANCE":["BRE","MAR","PAR","MAO","BUR"],"GERMANY":["BER","MUN","HOL","KIE","TYR"],"ITALY":["NAP","ROM","ION","VEN","TRI"],"RUSSIA":["WAR","MOS","SEV","STP","RUM","BOT"],"TURKEY":["ANK","SMY","CON","BUL"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE - TRI","F ALB S A VIE - TRI","A SER - BUL"],"ENGLAND":["F NWG - NWY","A YOR - BEL VIA","F NTH C A YOR - BEL"],"FRANCE":["A MAR H","F MAO H","A BUR H"],"GERMANY":["F HOL - BEL","A KIE - DEN","A TYR H"],"ITALY":["F ION - TUN","A VEN - TYR","A TRI S A VEN - TYR"],"RUSSIA":["A WAR - GAL","F RUM H","A STP - FIN","F BOT - SWE"],"TURKEY":["A SMY - CON","A BUL - GRE","F CON - BUL\/SC"]},"results":{"A VIE":[],"A SER":["bounce"],"F ALB":[],"F NWG":[],"F NTH":[],"A YOR":["bounce"],"A MAR":[],"F MAO":[],"A BUR":[],"F HOL":["bounce"],"A KIE":[],"A TYR":[],"F ION":[],"A VEN":["bounce"],"A TRI":["cut","dislodged"],"A WAR":[],"A STP":[],"F RUM":[],"F BOT":[],"A SMY":["bounce"],"F CON":["bounce"],"A BUL":[]},"messages":[{"sender":"ITALY","recipient":"GLOBAL","time_sent":180489,"phase":"F1901M","message":"wow, what a first turn! lots of unusual openings..."},{"sender":"ITALY","recipient":"GERMANY","time_sent":180650,"phase":"F1901M","message":"dear sir, the italian nation does not take kindly to your incursions into tyrolia. can you please explain?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":180678,"phase":"F1901M","message":"well, that's a problem. i don't understand why germany would be so anti-france and then pull that move. oh well."},{"sender":"ITALY","recipient":"FRANCE","time_sent":180841,"phase":"F1901M","message":"looks like after all blubbering germany did about wanting to take you down, he decided to much things up with his southern neighbors instead. good break for you!"},{"sender":"ITALY","recipient":"FRANCE","time_sent":180910,"phase":"F1901M","message":"there's no way i want to see him in pie, so that's not an option. i hope we can work together against G."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":181006,"phase":"F1901M","message":"looks like a very promising opening for you. nicely done. i have no clue what germany thinks he can gain by moving to tyr, so that's a major headache."},{"sender":"ITALY","recipient":"TURKEY","time_sent":181070,"phase":"F1901M","message":"with you working with russia, i can't really fight austria-hungary. espeically not with germany all up in my grill."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":181128,"phase":"F1901M","message":"well, it looks like denmark is wide open for you, but russia could bounce you at norway. i wonder what will happen. i'm curious about why you moved to yor and not edi - edi would give you more options now."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":182591,"phase":"F1901M","message":"You are right, I had Edi at first but moved to York in case France went into the channel."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":182681,"phase":"F1901M","message":"Looks like Russia\/Turkey are trying a Juggernaut. Can I count on support from you into Belgium?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":182682,"phase":"F1901M","message":"fair enough."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":182734,"phase":"F1901M","message":"I'm with you against Germany, I don't understand his moves though, what was he trying to accomplish? <br \/><br \/>Could I count on support into Belgium from Yorkshire?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":182834,"phase":"F1901M","message":"Well Sweden is yours but what designs do you have from St. P? It doesn't look very friendly."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":182876,"phase":"F1901M","message":"Thanks for keeping up your end. England has assured himself that he can enter Norway....but it will require all of his units to get in."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":182893,"phase":"F1901M","message":"Great job on Trieste, I am very confused by Germany's move into Tyrolia though, what do you think he is trying to accomplish?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":182996,"phase":"F1901M","message":"it came as a complete shock to me, and i'm very upset with it. i might have to make peace with austria, since there also seems to be a jugglenaut (R\/T) forming. that's really bad news. basically, the east is a total fuck up."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":183023,"phase":"F1901M","message":"after all of germany's anti-french blabbing, then he goes and sticks his head where it doesn't belong. we had agreed to tyrolia DMZ."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":183071,"phase":"F1901M","message":"Well...I don't think we talked enough prior to the opening moves....so I thought that might bring you to the negotiating tables. How is your relationship with Germany and France? Are you able to identify either one of them as a solid ally...or are they both against you. Just trying to get a feel for the relationships in the West."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":183536,"phase":"F1901M","message":"The communications and the moves that followed didn't correspond at all except mine which I made clear to them. I intend on moving my army to Belgium which is why I didn't move him to Edinburgh so you wouldn't think I was moving it to norway to be offensive to you. <br \/><br \/>I was actually made homeless on Tuesday so my time on this site while can happen due to free wi fi, isn't going to be as plentiful as I'm accustomed to."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":183686,"phase":"F1901M","message":"They are a blatant Juggernaut and I've taken part in many successful ones myself so in terms of the Russian North aspect of it I can stop it without being tag teamed to my south. Germany needs to get his shit together or he will be speaking Russian soon enough."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":183782,"phase":"F1901M","message":"yeah i'd say support yourself into norway, using the army, is that what you're thinking?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":183794,"phase":"F1901M","message":"The Juggernaut is very blatant so I'm not missing that cue. I've taken part in several so I know the dynamics and how effective they may be."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":183929,"phase":"F1901M","message":"No because I suspect he will convoy the army into Sweden or move into Finland. He can bounce me but accomplishes nothing and he will need a fleet in St. P NC or I will be able to nip his north real fast."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":184104,"phase":"F1901M","message":"ah, i see."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":185002,"phase":"F1901M","message":"Homeless??? Crap, man.... You have bigger issues then. Being here is definitely not a priority then. Jeez.... I hope things work out for you.<br \/><br \/>But honestly, it gives me concern if you will be able to get orders on time. I'd be worrying if my ally was going to go CD or not. It appears France and Germany are allied against you."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":187074,"phase":"F1901M","message":"Looks like you and France are trying the same thing(ie. no action in the Channel). Also, Italy did not attack.........strange."},{"sender":"GERMANY","recipient":"FRANCE","time_sent":187090,"phase":"F1901M","message":"liar."},{"sender":"GERMANY","recipient":"ITALY","time_sent":187319,"phase":"F1901M","message":"Yes. England assured me that you and France were teaming up against me. I knew that France was totally untrustworthy, and would support his unit into Bur(as opposed to the bounce that he suggested). Had I attempted to bounce in Bur, and had you waltzed into Tyr, then I would have needed to tie up two units in the autumn in order to defend Mun."},{"sender":"ITALY","recipient":"GERMANY","time_sent":187699,"phase":"F1901M","message":"why would you believe england? didn't austria confirm that i wasn't gunning for you? why on earth would i attack you? that's the stupidest opening possible for an italian."},{"sender":"GERMANY","recipient":"ITALY","time_sent":187763,"phase":"F1901M","message":"You've done it before. Stop posturing please."},{"sender":"ITALY","recipient":"GERMANY","time_sent":187769,"phase":"F1901M","message":"you know as well as i do that the central powers don't do well when we fight. i was actually going to do a three fleets variant opening with austria in order to get two additional fleets to attack france with. but now that you've moved to tyrolia, i can't risk that."},{"sender":"ITALY","recipient":"GERMANY","time_sent":187814,"phase":"F1901M","message":"i've done it before because i was stupid. <br \/><br \/>and who's posturing, you're the one who moved into tyrolia after we explicitly agreed not to. do you do everything that england tells you to do?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":187817,"phase":"F1901M","message":"All you and A needed to do was tell me that you were doing the Lepanto."},{"sender":"ITALY","recipient":"GERMANY","time_sent":187852,"phase":"F1901M","message":"nicely done, you've messed everything up."},{"sender":"ITALY","recipient":"GERMANY","time_sent":187885,"phase":"F1901M","message":"and risk you telling it to someone else? since it's clear now that you can't be trusted, i suppose we made the right decision."},{"sender":"GERMANY","recipient":"ITALY","time_sent":187933,"phase":"F1901M","message":"Wow. You're emotional. I wonder why......."},{"sender":"ITALY","recipient":"GERMANY","time_sent":187984,"phase":"F1901M","message":"good one. you really know how to charm people over, don't you?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":188008,"phase":"F1901M","message":"violate your word, then insult them with stupid stereotypes."},{"sender":"GERMANY","recipient":"ITALY","time_sent":188017,"phase":"F1901M","message":"flashman has played with me before, as an ally. <br \/><br \/>I'm curious. CAN YOU figure out what my autumn order for Tyr will be? Think hard, Trixie."},{"sender":"ITALY","recipient":"GERMANY","time_sent":188025,"phase":"F1901M","message":"a real winning strategy."},{"sender":"ITALY","recipient":"GERMANY","time_sent":188059,"phase":"F1901M","message":"yeah i know what it will be, douche bag."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":188108,"phase":"F1901M","message":"anyway, you've got germany opened up wide for an attack now, along with france. i know i'd be glad to see it, considering his violation of our agreement."},{"sender":"GERMANY","recipient":"ITALY","time_sent":188116,"phase":"F1901M","message":"Grow up...."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":188148,"phase":"F1901M","message":"Good luck with your ally. She's a hothead."},{"sender":"GERMANY","recipient":"ITALY","time_sent":188234,"phase":"F1901M","message":"BTW, why Rom-Ven? Seems suspect."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":188243,"phase":"F1901M","message":"I said Italy seemed more France friendly. If I went in the channel it wouldn't accomplish anything until I get builds. I'm not sure what you did though, at least your borders are fairly calm, I think I might get bounced out of two builds lol"},{"sender":"ITALY","recipient":"FRANCE","time_sent":188273,"phase":"F1901M","message":"if you support E into bel, and then take spa and mar, you'll be in good shape. i'm hesitant to throw germany out of tyr lest he end up in pie, but i am pretty sure he'll just move back to mun expecting to bounce you there.<br \/><br \/>still, i'm playing the enraged role, so he might do something more unpredictable if i get him annoyed enough with me. who knows. i am hopping mad at him, but i guess i'm more thinking that if i keep badgering him with how angry i am, then that might make him take an ill-considered move. probably not, i suppose, and i don't know what move i would want to see really..."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":188280,"phase":"F1901M","message":"BTW. Why Rom-Ven? Hardly a standard Lepanto opening....."},{"sender":"ITALY","recipient":"GERMANY","time_sent":188307,"phase":"F1901M","message":"grow a brain and maybe you could figure it out, dumbo."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":188320,"phase":"F1901M","message":"Go for the certain build in Nor."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":188348,"phase":"F1901M","message":"He will be attacked, I expect to have a fun time playing in the north."},{"sender":"GERMANY","recipient":"ITALY","time_sent":188373,"phase":"F1901M","message":"You're in what? Grade 6...Grade 7?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":188476,"phase":"F1901M","message":"I know I have bigger things to work on but I haven never gone CD the most was miss a turn. This game is a much needed calming agent.<br \/><br \/>I can't tell what France and Germany are doing, I am keeping my moves neutral until I'm attacked which may be by all 3 neighbors before long."},{"sender":"GERMANY","recipient":"ITALY","time_sent":188481,"phase":"F1901M","message":"All of this venom. Is it the Ritalin?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":188562,"phase":"F1901M","message":"Thank you for keeping your word as well. I will need your help versus Eng-Fra for sure."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":188572,"phase":"F1901M","message":"yeah, i think so! you might even be able to get russia in on the job."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":188668,"phase":"F1901M","message":"germany is being a complete ass in his press to me."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":188717,"phase":"F1901M","message":"what would the moves for this turn be if we went with the three fleets plan? and are you interested in considering any other options like a key lepanto?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":188787,"phase":"F1901M","message":"i'm just curious, it doesn't affect anything, but germany said you told him i was working with france against him? a lie from him, or from you? i don't care if you did tell him, it wasn't true, but doesn't really hurt me at all. just funny that he completely fell for it, if it was you ;D"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":188825,"phase":"F1901M","message":"Well?................"},{"sender":"GERMANY","recipient":"ITALY","time_sent":188943,"phase":"F1901M","message":"A good anger management strategy is to take deep breaths and count to 50."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":189193,"phase":"F1901M","message":"No answer, and you're still logged on( but I'M not very talkative......)"},{"sender":"GERMANY","recipient":"ITALY","time_sent":189254,"phase":"F1901M","message":"Still no response?<br \/><br \/>Where's the love?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":189324,"phase":"F1901M","message":"It was me ^_^ I tell everyone something or another."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":189376,"phase":"F1901M","message":"I'm homeless as of yesterday morning. I have an excuse to be focused elsewhere."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":189419,"phase":"F1901M","message":"lol"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":189435,"phase":"F1901M","message":"well, a good one. he bought that one hook, line, and sinker. =P"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":189444,"phase":"F1901M","message":"Sicarius?........Is that you?"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":189490,"phase":"F1901M","message":"Do not depend on France's support into Bel. He will likely go for Munich...."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":189577,"phase":"F1901M","message":"As of now, you are the only Power that I trust....."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":195652,"phase":"F1901M","message":"Thanks. Honestly, I was concerned you would send me North and send your army East. I breathed a huge sigh of relief when I saw the open. My plan is to move STP to FIN and build a fleet in STP (NC)...so that I can swing SWE to NOR.....which will open the NS to you."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":201293,"phase":"F1901M","message":"Yes, the German is being a bit tricky. However, as long as you hit him from Venice, I can do the job on your army in Trieste. The Russian did as I had hoped so Budapest is safe."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":201340,"phase":"F1901M","message":"I haven't agreed on a Lepanto! This is treachery of the highest order. What will you do with Tyrolia?"},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":201395,"phase":"F1901M","message":"Please note, because of the Rome to Venice move, you are not seeing a Key Lepanto (where you get attacked) but treachery against me by Italy, where I get attacked..."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":205963,"phase":"F1901M","message":"Glad to see a peaceful start for us. I already have England whining about the Juggernaut.....I said \"what's a Juggernaut?\" :)"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":206758,"phase":"F1901M","message":"lol didn't he get \"arrested\" months ago :P"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":209196,"phase":"F1901M","message":"Thanks...same to you. Even if you get dislodged, you retreat to Budapest. Has Germany clarified what he is doing south? I imagine he is quite distrustful of the French and will need to cover MUN."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":248110,"phase":"F1901M","message":"i would indeed be shocked if he did anything but move to munich."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":248247,"phase":"F1901M","message":"what's your game this turn? looking good in sweden - are you planning to bounce the englishman?<br \/><br \/>even though i'm sure he's moving back to munich, the german has really gotten on my nerves (didn't inform austria or i of the move to tyr, was a jerk from the get go in his press about it [tho that's no surprise with mapleleaf]) so i might well help france against him. but that does depend, in part, on your game plan."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":248354,"phase":"F1901M","message":"yes, he had mentioned possibly letting you into gal, but i suggested that was not a good idea. <br \/><br \/>i'll send ven-tyr, how exactly are you going to take out tri? should i go for greece? or are you going to? i would hate to see turkey take it. i'm also a bit concerned that turkey and russia do seem to be working together."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":248386,"phase":"F1901M","message":"should that alter our game plan at all? are you ok handling them both on your own?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":248431,"phase":"F1901M","message":"Hardly unusual for France and England and Turkey and Russia... It is the German move that sucks. If we are really doing this Three Fleets (and I will not change my plans or promises) I will be delighted, but the map does look a tad like a stitch up, with Germany having ben shown the plans. Please tell me that he has acted independently. I will trust you, once... ; )"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":248543,"phase":"F1901M","message":"he told me that england had said i was working with france to take him down. england confirms. <br \/><br \/>ask the german about my interactions with him, i'm sure you'll hear they've been less than pleasant. not exactly ally-like. that particular player very much gets on my nerves, and his move to tyrolia is so frigging annoying, after we explicitly agreed to keep it free."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":248573,"phase":"F1901M","message":"as for three fleets - i'm perfectly content with doing it. i am also completely open to taking another approach."},{"sender":"ITALY","recipient":"TURKEY","time_sent":248640,"phase":"F1901M","message":"nonetheless, i'm pretty sure AH will use energy to evict me from trieste, so you should be able to take greece."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":248690,"phase":"F1901M","message":"Not to worry then, as long as you are not working with him, his single unit should not be a problem for now. I though will have to take Trieste with the fleet as this is the only safe way. Vienna cannot be guaranteed to support if germany hits me there. That means Greece is a possible shot for you, although Tunis is the safe option."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":249129,"phase":"F1901M","message":"i don't think he will hit you at vienna - he has to cover munich, no? and we've had such an exchange of words that i think if anything he'll hit me at ven not you. i think you should be safe with using vienna to support, but it's up to you, obviously."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":249440,"phase":"F1901M","message":"I will look carefully. Be assured though that I will see the plan through. It matters not how I hit Trieste as long as you get the chance to disband (and can avoid that temptation to retreat).<br \/><br \/>So, you are the real deal, a female Dip player? I've only know two others here for certain. Akroma, who is doing very well at the moment, and a girl called Ozidip who left ages ago: she was marvelously entertaining because she would get all weepy if things went wrong. She was Italian and wrote strange English. I enjoyed our games together: we cooperated a few times but I think the first stab was one to many for her."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":249657,"phase":"F1901M","message":"well i just want to try to limit turkey as much as possible. i don't want to have him mess up your plans, and i don't want to have him take greece, if possible. so that's why i'm asking about how exactly you are taking trieste. i believe you'll follow through (there's no incentive for you not to do so) and so will i - i would be stupid to pick a fight with you after going off on germany. what i think the advantage of that situation is, though, is that since i'm being so antagonistic against germany, france most likely will not see the 3 fleets coming. i also am hoping i can get it to get france and england working together - if france builds armies, that helps me a lot in terms of getting out into the atlantic."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":249820,"phase":"F1901M","message":"as for gender, yes i'm a girl. i'm sad there's not more girls who play the game, but i understand why that is. i've met a few on here besides akroma, and i am actually trying to organize an all-girls game but only have 6 players so far. the macho culture of the site gets to me at times, so i would like to play a game without that. i also find that i need to not ever post in the forum about things unrelated to the game b\/c people really bother me with what they write, which is fine, b\/c i come here for some entertainment in the form of a game, not to debate social topics."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":250015,"phase":"F1901M","message":"England can force himself into Norway, if he wanted. So I will not oppose his entry. Why make enemies when he foil me anyway? I will attempt Galicia again this turn."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":250021,"phase":"F1901M","message":"Hopefully, I will manage to remain a true gentleman here. Do you know anything about my chosen name? Flashman is a literary figure who was made out to be contemporary with the great statesmen of the nineteenth century. He was also a complete and utter womaniser who would lie and deceive to get away from any form of danger. The books are great reading."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":250060,"phase":"F1901M","message":"no, i've never heard of it. it's a series of books? who is the author? <br \/><br \/>my name is simple, it's my dj name. =P"},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":250090,"phase":"F1901M","message":"I ask again you scoundrel. Have you an agreement with Italy or do you still think I owe you money?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":250133,"phase":"F1901M","message":"true. i get the impression he's not going to force it, but he's told a few fibs already, so who knows."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":250159,"phase":"F1901M","message":"George MacDonald Fraser"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":250250,"phase":"F1901M","message":"He died recently. The books are very accurate about the situations and personalities they contain, with the one about the Charge of The Light Brigade being standard reading at Uni for its particularly well-researched description of that battle. They are thus very educational and also funny, hugely funny."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":250292,"phase":"F1901M","message":"heh, i'll have to check them out. i've been needing some new reading lately."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":250315,"phase":"F1901M","message":"russia says he is planning to move to gal again, btw. not sure how that affects your plans."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":250403,"phase":"F1901M","message":"I might have to let him take it. I will though try to dissuade him."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":250472,"phase":"F1901M","message":"Okay, now you are here - can we try the DMZ in Galicia this time? It would enable both of us to use our units for other purposes. We both have business to attend to. Me, in particular!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":250498,"phase":"F1901M","message":"alright. i don't suppose you'd want to support me into greece? i know you are afraid of the german hitting vienna and cutting support, but i think that's really unlikely."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":250628,"phase":"F1901M","message":"back to literature - i read melville's billy bud a long time ago and i thought it was fantastic. don't know if i would like it if i read it again, but i'd suggest it based on my memory of it."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":250646,"phase":"F1901M","message":"That is not a bad idea: you get the build and Turkey gets stopped. However, it leaves one of your fleets on the wrong side to attack France and speed is important for this opening."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":250768,"phase":"F1901M","message":"that certainly is true. i can get into wes easily from tunis, that's for sure. maybe you can simply bounce with turkey there? i've suggested he move there already. i'll let you know what i hear from him."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":251072,"phase":"F1901M","message":"so what's new mr talkative? how are things between you and france? hatching plans for the grand invasion of germany yet? russia has told me he's not going to contest norway, so that's good news for you. are you and him going to cooperate against the kraut?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":251264,"phase":"F1901M","message":"Okay, thanks."},{"sender":"ITALY","recipient":"FRANCE","time_sent":251794,"phase":"F1901M","message":"i'm moving to tyr, so it might be good if you don't try for mun and bounce him, so that he ends up back there and we'll have him surrounded."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":252332,"phase":"F1901M","message":"And what of the Frogs? Surely they are our blood enemies."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":253912,"phase":"F1901M","message":"With my current situation, being talkative has been sapped out of me :P I'll get back on my feet and be more flaming optimistic in no time but for now I'm fighting capitalism.<br \/><br \/>France stopped replying to me and Germany communicates like a jackass so he is becoming much more of a target. Russia showed intent to bounce Norway when I showed awareness of the Juggernaut and refused to respond. He might not to take Germany out which would be his better option (for me and\/or him) but I am not sure if he wants that option."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":254185,"phase":"F1901M","message":"he seems inclined that way. germany is a jackass, that's just a fact of life - learn to live with it and take advantage of it, and you'll do fine. i'm fighting him for precisely that reason, tho. i'm sure you'll hear from france, i don't think he was online much yesterday."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":254263,"phase":"F1901M","message":"Still no answer. I'm not in Mensa, but I can still make an educated guess at what your refusal to acknowledge my Norway suggestion means....."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":254444,"phase":"F1901M","message":"The suggestion that I have an agreement with Italy is hilarious. She may be working a triple with Eng and Fra. Tyr will likely have to cover Mun, unfortunately. <br \/><br \/>I could tell that Chrisp was lying because his fingers were moving...."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":254530,"phase":"F1901M","message":"Please do not. Eng and Fra are working together. Eng will order Yor-Bel supported by Bur, and Nrg-Nor unsupported. You can bounce him."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":255203,"phase":"F1901M","message":"I noticed. If him being a jackass is a fact of life I feel sorry for him lol"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":255323,"phase":"F1901M","message":"Not refusal.. I have been distracted with outside of this game and telling rlumley he is an idiot lol<br \/><br \/>I plan on moving into Norway and Belgium, if I get no support and get bounced out of both it'll show me I have very limited options in this game and it doesn't bode well. Otherwise it will show me what directions I can take, if I get in Belgium and Russia bounces me I plan on pile driving into his territory regardless of how it leaves my defenses."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":255367,"phase":"F1901M","message":"Also, I'm not in Mensa. They let in just about anyone who can pass their silly test :P They focus on dues and not content, I'm a triple 9 society fan."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":255404,"phase":"F1901M","message":"message from England follows....<br \/>=============================================================<br \/>I plan on moving into Norway and Belgium, if I get no support and get bounced out of both it'll show me I have very limited options in this game and it doesn't bode well. Otherwise it will show me what directions I can take, if I get in Belgium and Russia bounces me I plan on pile driving into his territory regardless of how it leaves my defenses."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":255503,"phase":"F1901M","message":"What does France have to say about your Belgium plans?"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":255560,"phase":"F1901M","message":"The fact that you not even bothered to ask me for support is telling.............."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":255664,"phase":"F1901M","message":"Beware Italy. She is playing both sides to her benefit. My suspicion is that she is working within two triples. Eng-Fra-It, and Rus-Tur-It."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":255747,"phase":"F1901M","message":"Italy may also be working another triple with Rus and Tur."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":255770,"phase":"F1901M","message":"I suspect that she is working both, to her credit."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":255827,"phase":"F1901M","message":"Yes I did, scroll very far up! lol"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":255879,"phase":"F1901M","message":"France hasn't said anything more than 4 lines to me. He hasn't been logged on very much. I think he is taking the 50\/hr phases very seriously lol"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":255880,"phase":"F1901M","message":"ahaha true"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":255983,"phase":"F1901M","message":"Russia is forming a Juggernaugt and I confronted him and a Juggernaut entails that he will build in Warsaw and\/or St. P fleet if not it isn't a good one. So if he bounces back into both Warsaw and St.P he will have seriously slowed any progress so I see Turkey stabbing him mid-Balkans"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":256756,"phase":"F1901M","message":"I apologize. You are right. If I support you into Bel, then what's in it for me?"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":257388,"phase":"F1901M","message":"One less person to worry about? I have been progressing neutral moves and intend on striking at who is aggressive towards me, not support me won't be considered aggressive. If you stay neutral and france and\/or Russia bounces me I'll have no grudge towards you and move towards the attacker(s). Pretty much it's still early in the game and I do not have a great rapport with anyone which is fine, mutual gain games work best anyways."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":257466,"phase":"F1901M","message":"For me at least. My grammar died in that paragraph lol I'm moving yet again to stay with other friends so I'm distracted with packing. It'll be a bit before I get a place of my own and\/or semi-permanent."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":262159,"phase":"F1901M","message":"Thanks for the heads up on Italy.... Though I can tell you with absolute certainty that she is in no triple alliance that I am part of. Though there is some room to work with her in terms of Austria. However I haven't committed to any course of action against Austria yet.<br \/><br \/>Now .... Back to England. I think you may not be considering the longer term play here. A bounce means no northern fleet build in STP. While a move to Finland gives me a 3 to 2 advantage immediately in the spring to have the needed fleet take Norway. He will be unable to do anything with his fleet but to defend Norway.....plus it gives us the element of surprise when your fleet takes the north sea in the fall. Consider this.... And let me know if you still want me to bounce."},{"sender":"TURKEY","recipient":"ITALY","time_sent":262345,"phase":"F1901M","message":"This is good news for me."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":262373,"phase":"F1901M","message":"very well. Are you looking to take Trieste back?"},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":262439,"phase":"F1901M","message":"I guess what I'm really asking is should I move to Gre or not?"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":262475,"phase":"F1901M","message":"Very nice.<br \/><br \/>I may get Gre this turn, we'll see how it goes."},{"sender":"ITALY","recipient":"TURKEY","time_sent":262691,"phase":"F1901M","message":"i guess he'll get german help to cut my support, but at least i can retreat to bud, right? =)<br \/><br \/>and two builds in the first year is good for turkey!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":263682,"phase":"F1901M","message":"it seems turkey is moving to greece. so i think you'll be able to bounce him."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":263771,"phase":"F1901M","message":"so the plan is still to take on turkey once austria is done, right? <br \/><br \/>i've told him he can take greece for now, so he's getting two builds, just as a heads up to you."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":283751,"phase":"F1901M","message":"Supporting you into an SC adjacent to one of my own does not equate to \"one less person to worry about\". Rather, it equates to one more."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":283789,"phase":"F1901M","message":"Do you have any suggestions regarding Tyr?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":284092,"phase":"F1901M","message":"I would prefer you to bounce, because Eng may simply go all out for me, along with France and Italy's support. That would be 15 units for me to hold off. <br \/><br \/>You still get Nor next year, if you bounce."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":285048,"phase":"F1901M","message":"I do have a problem with Trieste and the German is not helping. I cannot move to Greece."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":285192,"phase":"F1901M","message":"Hit Venice... ; ) I have to get back into Trieste so my options are limited. You though have the problem of Burgundy. You can bounce in all sorts of ways but doing so with Kiel give England Denmark (or Holland if he guesses correctly)."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":285240,"phase":"F1901M","message":"There are many possibilities here for treachery, the one thing I am sure of is that Trieste is supposed to be Red. So, if anyone is in trouble it is me right now."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":290628,"phase":"F1901M","message":"I will move to Greece then, if only to prevent Italy from taking it."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":296544,"phase":"F1901M","message":"Okay."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":297033,"phase":"F1901M","message":"If that's how you see it then you play in a very lonely manner."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":335147,"phase":"F1901M","message":"No, I play with allies, enemies, and neutrals. I am asking you to ally with me, because of a touchy territorial situation."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":336888,"phase":"F1901M","message":"anything new? not much in my neck of the woods. everyone is being pretty quiet, which is sad."},{"sender":"ITALY","recipient":"GERMANY","time_sent":336918,"phase":"F1901M","message":"you will have all my love once your army is squarely back in munich =)"},{"sender":"ITALY","recipient":"TURKEY","time_sent":336937,"phase":"F1901M","message":"anything new? everyone seems to be a bit quiet."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":337000,"phase":"F1901M","message":"if you wanted to, i suppose you could bounce turkey at bul so that he doesn't get two builds, but just one. but you don't seem to have any enemies just yet, and are on route to 2 yourself, so i imagine you might not want that. with your northern opening i assume you are not trying to fight with turkey?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":337041,"phase":"F1901M","message":"i'm a little concerned that you are going to NMR\/CD in this game chrisp, what's going on with that? i guess something must have come up in your life,hope everything is alright."},{"sender":"GERMANY","recipient":"ITALY","time_sent":337125,"phase":"F1901M","message":"Well, that won't be long then. <br \/>:0)"},{"sender":"ITALY","recipient":"GERMANY","time_sent":337341,"phase":"F1901M","message":"super. looks like you'll get a nice break and have an NMR france."},{"sender":"GERMANY","recipient":"ITALY","time_sent":337721,"phase":"F1901M","message":"I doubt it. Chrisp is just amusing himself. I guessed wrong to Tyr. I should have ordered Mun-Ruh, obviously."},{"sender":"ITALY","recipient":"GERMANY","time_sent":337791,"phase":"F1901M","message":"he just went CD in another game i am in with him, that's what i mention it. but you could be right."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":342520,"phase":"F1901M","message":"Well....we did agree to take Austria first, then Turkey. Are you having 2nd thoughts?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":343094,"phase":"F1901M","message":"with you opening north, it's going to be hard to do that. and with germany in the mix. but let's see how things unfold."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":343381,"phase":"F1901M","message":"The thing about though, is that it is not just you and me against Austria....it is you me and Turkey....which even with my opening North, we are 8-3 against him."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":343443,"phase":"F1901M","message":"well honestly that's what worries me a bit. once austria is gone, with turkey in the mix, maybe you will decide to let turkey loose on my and focus on your northern sphere."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":343903,"phase":"F1901M","message":"This goes back to our opening discussion. I kind of feel like we are retracing old ground here. The Juggernaut is an easy sell for Turkey. He is more likely to trust that kind of alliance, then a A\/I alliance in which the only common enemy is Russia....because Austria needs Turkey to fight Russia. And since we had agreed to this course of action....I felt good about the northern open."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":344067,"phase":"F1901M","message":"I thought we were from the beginning, I've just been worried about Russia going for me\/you due to a juggernaut."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":344137,"phase":"F1901M","message":"fair enough."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":351167,"phase":"F1901M","message":"I have considered at length what you have asked.... And I very much want to keep peace between us. However, I plan to move as I have already stated. Getting the 2nd fleet is important for my defense. I will not build in WAR if that eases your mind any. I am acting because I truly feel this is the better way to defeat England. It doesn't make sense for Italy to attack you. And the maginot line will be difficult for France to crack."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":359272,"phase":"F1901M","message":"OK. Fair enough."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":359599,"phase":"F1901M","message":"i'm sorry but i think i'm going to take austria's offer of peace for now."}]},{"name":"F1901R","state":{"timestamp":1537459322932800,"zobrist_hash":"484788823214292870","note":"","name":"F1901R","units":{"AUSTRIA":["A SER","F ALB","A TRI"],"ENGLAND":["F NTH","A YOR","F NWY"],"FRANCE":["A MAR","F MAO","A BUR"],"GERMANY":["F HOL","A TYR","A DEN"],"ITALY":["A VEN","F TUN","*A TRI"],"RUSSIA":["F RUM","A GAL","A FIN","F SWE"],"TURKEY":["A SMY","F CON","A GRE"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","VIE","SER","ALB","TRI"],"ENGLAND":["EDI","LON","LVP","NWG","NTH","YOR","NWY"],"FRANCE":["BRE","MAR","PAR","MAO","BUR"],"GERMANY":["BER","MUN","HOL","KIE","TYR","DEN"],"ITALY":["NAP","ROM","ION","VEN","TUN"],"RUSSIA":["WAR","MOS","SEV","STP","RUM","BOT","GAL","FIN","SWE"],"TURKEY":["ANK","SMY","CON","BUL","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":["A TRI D"],"RUSSIA":[],"TURKEY":[]},"results":{"A TRI":["disband"]},"messages":[{"sender":"ITALY","recipient":"GERMANY","time_sent":360667,"phase":"F1901R","message":"well, i guess you enjoy your lies?<br \/><br \/>sigh."},{"sender":"GERMANY","recipient":"ITALY","time_sent":360739,"phase":"F1901R","message":"It's not like that at all. I just want to pal around with you, and help you versus Austria."},{"sender":"GERMANY","recipient":"ITALY","time_sent":360820,"phase":"F1901R","message":"Think of everything that we could do together, with me in Tyr!"},{"sender":"ITALY","recipient":"GERMANY","time_sent":361074,"phase":"F1901R","message":"lolz"},{"sender":"ITALY","recipient":"GERMANY","time_sent":362164,"phase":"F1901R","message":"well i have seen italy and germany work together quite well..."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":364082,"phase":"F1901R","message":"OK....Will you go for the wide open spaces of France or try to pentrate Turkey?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":368921,"phase":"F1901R","message":"i guess i was thinking of france. but i am rethinking it all, since france is wide open, and germany didn't leave tyr, i may have to reconsider."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":372933,"phase":"F1901R","message":"That wasn't necessary..."},{"sender":"TURKEY","recipient":"ITALY","time_sent":373014,"phase":"F1901R","message":"I've been stuck in bed the last few days with a cold.<br \/><br \/>I can't comment on anyone else.<br \/><br \/>Not happy with Austria. :)"},{"sender":"FRANCE","recipient":"GLOBAL","time_sent":380931,"phase":"F1901R","message":"Ugh. I´m in Cancun now, and I do not have internet at my hotel. Sorry all."},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":381290,"phase":"F1901R","message":"Free SC's!!!"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":381429,"phase":"F1901R","message":"Thank you very much for the lack of bounce How do you feel about me supporting Sweden into Denmark? That is if St. P remains empty and a mutual divide of Germany and his aggression."},{"sender":"FRANCE","recipient":"GLOBAL","time_sent":381430,"phase":"F1901R","message":"Not quite, haha. I plan to get to an internet cafe or a wifi spot at least once a day."},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":381545,"phase":"F1901R","message":"lol ^_^"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":381672,"phase":"F1901R","message":"Alright. I have been slowed down, but I can still support you into Belgium if you are still with me here. With Russia coming at you in the North I presume you want to build F-Edi. Do you think Russia and Germany are working together?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":381794,"phase":"F1901R","message":"Ack. Ok well, I am back now. What is the situation in the east? Have any alliances fallen out of the chaos yet? I have some guesses but if you have information, that would be great."},{"sender":"FRANCE","recipient":"GERMANY","time_sent":381840,"phase":"F1901R","message":"I kept hearing rumours about an E-G alliance, so I had to play it safe. It might interest you to know who was spreading the rumours."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":381855,"phase":"F1901R","message":"I have had suspicions. His bounce was him saying he wants an alliance apparently lol. I would not mind the support at all but am curious as to where they are going to build."},{"sender":"FRANCE","recipient":"AUSTRIA","time_sent":381858,"phase":"F1901R","message":"Have you got a meaningful alliance sorted?"},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":381901,"phase":"F1901R","message":"It has been after the turn. Lets talk. You have two builds coming, where are you going to put them?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":382503,"phase":"F1901R","message":"Haha, wait, his bouncing of you in Belgium was supposed to indicate that he wanted to be in an alliance with you? Classic mapleleaf."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":383634,"phase":"F1901R","message":"That was terrible timing to miss a turn. How can I help you?"},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":392040,"phase":"F1901R","message":"I might have. I managed to see off the Italian threat."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":392075,"phase":"F1901R","message":"Why not? I was told that you and Russia were allied. That usually means death for Austria."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":392112,"phase":"F1901R","message":"I am though willing to reassess this. I was under great pressure there with the Italians in Trieste and a possible R\/T move against me."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":392131,"phase":"F1901R","message":"Not bad. I seem to have got the Turk angry..."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":421774,"phase":"F1901R","message":"That's what he said, which is amazing to me lol I'm thinking Russia will support Sweden to Norway and then support finland to St.p which is where I'd retreat to. So I'm trying to suggest to Germany I support him into Sweden if he supports me into Belgium, he wont get support and I just don't want to be bounced."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":423012,"phase":"F1901R","message":"Your reason for blocking Bul is quite sound and I perhaps would have done the same in your situation."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":423045,"phase":"F1901R","message":"However, I would also like to try and continue to work with you."},{"sender":"GERMANY","recipient":"ITALY","time_sent":430207,"phase":"F1901R","message":"No reason why we cannot do so in this game..."},{"sender":"GERMANY","recipient":"FRANCE","time_sent":430226,"phase":"F1901R","message":"Welcome back."},{"sender":"ITALY","recipient":"GERMANY","time_sent":430954,"phase":"F1901R","message":"so what are you thinking?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":440825,"phase":"F1901R","message":"well the truth is, i don't know. what are your guesses? i might be able to give some insight into them."},{"sender":"GERMANY","recipient":"ITALY","time_sent":445854,"phase":"F1901R","message":"Well, you'll retreat to Bud and get 2 builds. I'm thinking F Nap and A Rom, but so what, right? We are in a great position to wipe out France together."},{"sender":"GERMANY","recipient":"ITALY","time_sent":446329,"phase":"F1901R","message":"I can support you to Tri, if you support me into Vie in the autumn."},{"sender":"GERMANY","recipient":"GLOBAL","time_sent":446505,"phase":"F1901R","message":"I'll guard your sc's for you.<br \/>;0)"},{"sender":"ITALY","recipient":"GERMANY","time_sent":451540,"phase":"F1901R","message":"i like it. and i do think we can take down france easily. <br \/><br \/>but sometimes it sucks for germany and italy to take out austria right off the bat - leaves us with T and R to contend with... maybe we should focus on france first? i'm open to either."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":456447,"phase":"F1901R","message":"I appreciate that comment. As Austria, I have to be cautious."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":459746,"phase":"F1901R","message":"I hope you are in a good, adventurous mood today. We have the chance here to make a decent opening work."},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":469840,"phase":"F1901R","message":"I think if you help yourself, you would be helping me. I just hope you will not commit to any alliances with England or Germany, though I do not mind if you make use of any offered services from them.<br \/><br \/>How is your situation in the South?"},{"sender":"FRANCE","recipient":"TURKEY","time_sent":469861,"phase":"F1901R","message":"What is the situation for alliances in your area?"},{"sender":"FRANCE","recipient":"AUSTRIA","time_sent":469902,"phase":"F1901R","message":"So who is your new friend?"},{"sender":"FRANCE","recipient":"GERMANY","time_sent":469939,"phase":"F1901R","message":"I am sure that England has started to offer you some sort of deal so that you will save his ass in Scandinavia. What do you think of his deal?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":469968,"phase":"F1901R","message":"I will keep my guesses to myself for now. I think it is for the best."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":470032,"phase":"F1901R","message":"Ok, but he will get Sweden if Russia does what you expect. To be honest, I think Sweden will stay there. I think you have forgotten Russia has builds coming."},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":470067,"phase":"F1901R","message":"Can you give me any ideas on what you are thinking of building?"},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":496104,"phase":"F1901R","message":"Your nemesis..."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":511505,"phase":"F1901R","message":"Chris, since you have no builds coming....the information you are seeking sounds more like a 3rd party is asking. If I thought you needed to know for your planning I would release that info to you. Since you will be EXACTLY in the same position, we can talk after the build.<br \/><br \/>Italy did tell me she made peace with Austria, though."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":513804,"phase":"F1901R","message":"Ok ... How can I assure my northern border with you, though? Would you agree to no fleet build in Edinburg?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":524222,"phase":"F1901R","message":"Well it looks like you two are working together but all NAP's look like that so what do you recommend? <br \/><br \/>I was planning on building in London anyways so I can agree to that."}]},{"name":"W1901A","state":{"timestamp":1537459322934771,"zobrist_hash":"7230081534658331285","note":"","name":"W1901A","units":{"AUSTRIA":["A SER","F ALB","A TRI"],"ENGLAND":["F NTH","A YOR","F NWY"],"FRANCE":["A MAR","F MAO","A BUR"],"GERMANY":["F HOL","A TYR","A DEN"],"ITALY":["A VEN","F TUN"],"RUSSIA":["F RUM","A GAL","A FIN","F SWE"],"TURKEY":["A SMY","F CON","A GRE"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","VIE","SER","ALB","TRI"],"ENGLAND":["EDI","LON","LVP","NWG","NTH","YOR","NWY"],"FRANCE":["BRE","MAR","PAR","MAO","BUR"],"GERMANY":["BER","MUN","HOL","KIE","TYR","DEN"],"ITALY":["NAP","ROM","ION","VEN","TUN"],"RUSSIA":["WAR","MOS","SEV","STP","RUM","BOT","GAL","FIN","SWE"],"TURKEY":["ANK","SMY","CON","BUL","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":1,"homes":["BUD","VIE"]},"ENGLAND":{"count":1,"homes":["EDI","LON","LVP"]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":2,"homes":["BER","KIE","MUN"]},"ITALY":{"count":2,"homes":["NAP","ROM"]},"RUSSIA":{"count":2,"homes":["MOS","SEV","STP","WAR"]},"TURKEY":{"count":1,"homes":["ANK"]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD B"],"ENGLAND":["F LON B"],"FRANCE":[],"GERMANY":["A KIE B","A MUN B"],"ITALY":["F NAP B","F ROM B"],"RUSSIA":["A SEV B","F STP\/NC B"],"TURKEY":["F ANK B"]},"results":{"A BUD":[""],"F LON":[""],"A KIE":[""],"A MUN":[""],"F NAP":[""],"F ROM":[""],"A SEV":[""],"F STP\/NC":[""],"F ANK":[""]},"messages":[{"sender":"ITALY","recipient":"GERMANY","time_sent":546387,"phase":"W1901A","message":"shoot, i blew it. i missed the deadline - i couldn't decide whether to take on austria now or go with the stab, and i thought i would get on again before the end of the turn. oh well. i'll be going at france for now, but i'll be glad to turn on austria at some point in the future if it makes sense."},{"sender":"FRANCE","recipient":"AUSTRIA","time_sent":549642,"phase":"W1901A","message":"I assume you mean mapleleaf. Hahaha. You do not have any other friends? What about Turkey?"},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":549797,"phase":"W1901A","message":"It is fine if you do not want to tell me, but I like advance warning, and you are free to ask me similar questions. It helps me because I have to do some negotiating from my position and your two builds will factor heavily in the diplomatic decisions I make, as well as the builds I might suggest to other countries. If you have any builds you would like me to suggest to other countries, let me know. =)"},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":552589,"phase":"W1901A","message":"Italy does not seem to want to work with me anymore, which, at least gives me an idea what to watch out for in the Med. I'm not sure where Russia will build, but his build locations will give me some idea how I'd like to proceed."},{"sender":"TURKEY","recipient":"FRANCE","time_sent":552740,"phase":"W1901A","message":"Germany hasn't been very revealing. I would guess that you would be best served to start working with England to ensure you don't depart early.<br \/><br \/>As for me, I have offers from Russia and Austria, both of which have merits, both of which could pose some problems."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":552770,"phase":"W1901A","message":"Well, that wasn't what I was wanting."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":558897,"phase":"W1901A","message":"I think I have successfully negotiated my builds with my neighbors. As we have no common borders, I just don't see the need to divulge this to you. I wouldn't want to lie....and the truth may compromise my position...as I would expect you to negotiate in your best interest, rather than mine. Given you have no builds, I would expect you to create the friendliest of relationships among your neighbors. Builds will create another round of negotiations, I'm sure.<br \/><br \/>Seriously, though....I have spoken with my neighbors and have made the needed agreements."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":559290,"phase":"W1901A","message":"Yeah.....I am really sorry. I wasn't thinking, I guess. I could have easily supported you there....but you didn't ask.<br \/><br \/>Italy has made peace with Austria. So we will have to watch her moves particularly."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":559808,"phase":"W1901A","message":"Russia is sending promise of no build in St. P and vacating this fleet into the Baltic while his army moves in if I dont build Edi. Too good to be true or worth taking?"},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":562922,"phase":"W1901A","message":"No, Italy..."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":562983,"phase":"W1901A","message":"Do not worry about Italy. Help me with Russia and we can make it to the middle of this game. Just watch the Italian builds this time and then you will understand."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":596635,"phase":"W1901A","message":"Yea, I didn't think I'd need it. Ah well. Live and learn!<br \/><br \/>I've heard that Italy has made peace with Austria from Austria. Interesting tactic."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":596692,"phase":"W1901A","message":"Did the lackluster turn for France turn her attention towards the shiny unoccupied SCs? If so, that's a good thing for us."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":596728,"phase":"W1901A","message":"What do you think would be better for the current situation, a fleet or an army?"},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":596843,"phase":"W1901A","message":"Yes, a very good thing. Italy will be salivating: she gets to lay with three fleets this turn."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":596873,"phase":"W1901A","message":"er, lay = play. A somewhat Freudian slip there."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":597168,"phase":"W1901A","message":"somewhat.<br \/><br \/>To go after Russia, what would your plan be?<br \/><br \/>Obviously, I would need to take Black."},{"sender":"TURKEY","recipient":"ITALY","time_sent":597195,"phase":"W1901A","message":"Looks like France gave you a gift!"},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":597434,"phase":"W1901A","message":"Now that the position is clearer, we need to get you into Rumania. You will, of course, get Bulgaria as well, but Rum is the key. With builds, you can move on Sev and Russia is in trouble down south. My aim is to move up with you to take out Warsaw. We can discuss the long term future of Greece over a nice glass of Ouzo."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":597546,"phase":"W1901A","message":"If you move the army from Greece, I will not try to occupy the empty SC: I will leave it yellow unless you invite me to take it. Right now, Russia has to be hit before he gets Sweden and another unit."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":597626,"phase":"W1901A","message":"Now, the obvious concern on my mind is how do I know that you and Italy aren't conspiring to eliminate me?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":597630,"phase":"W1901A","message":"You are very fortunate here with the French failure to submit orders... Three fleets against a weak France? This game is going to be a safe one for you."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":597712,"phase":"W1901A","message":"Because Italy is actually playing the Three Fleets opening with me: it is the best way to attack France. Not only does she not have an army ready for a Lepanto, she also has a ridiculously weak France. She was bold enough to take my suggestion of the opening and has got lucky with actual moves."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":598554,"phase":"W1901A","message":"Fair enough.<br \/><br \/>Your feeling on my best build choice?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":601292,"phase":"W1901A","message":"I would say build a fleet. I can support you back to BUL and get CON into the Aegean."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":605320,"phase":"W1901A","message":"My thought was fleet as well, but I was concerned that you'd think it was a build that was outwardly aggressive towards you."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":606394,"phase":"W1901A","message":"In this case, we definitely need you to be able to get into the Med."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":607536,"phase":"W1901A","message":"That we do.<br \/><br \/>Thanks for making that easy!"},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":607575,"phase":"W1901A","message":"Russia is actually on board with me building a Fleet, which is a bit surprising."},{"sender":"GERMANY","recipient":"FRANCE","time_sent":621920,"phase":"W1901A","message":"I have no deal with Eng"},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":629982,"phase":"W1901A","message":"You need the second fleet to secure the Black Sea."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":632893,"phase":"W1901A","message":"thats what im hoping! sorry i haven't been communicative, it's been a really busy weekend."},{"sender":"ITALY","recipient":"TURKEY","time_sent":632945,"phase":"W1901A","message":"yeah, apprently. i missed my retreat phase, so got the disband, not the retreat, which is fine. you and russia can work on austria for now."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":639023,"phase":"W1901A","message":"Italy's words to me seem to indicate continued hostility towards you. Builds will tell the tale, I suppose."}]},{"name":"S1902M","state":{"timestamp":1537459322943980,"zobrist_hash":"4681844595716665213","note":"","name":"S1902M","units":{"AUSTRIA":["A SER","F ALB","A TRI","A BUD"],"ENGLAND":["F NTH","A YOR","F NWY","F LON"],"FRANCE":["A MAR","F MAO","A BUR"],"GERMANY":["F HOL","A TYR","A DEN","A KIE","A MUN"],"ITALY":["A VEN","F TUN","F NAP","F ROM"],"RUSSIA":["F RUM","A GAL","A FIN","F SWE","A SEV","F STP\/NC"],"TURKEY":["A SMY","F CON","A GRE","F ANK"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","VIE","SER","ALB","TRI"],"ENGLAND":["EDI","LON","LVP","NWG","NTH","YOR","NWY"],"FRANCE":["BRE","MAR","PAR","MAO","BUR"],"GERMANY":["BER","MUN","HOL","KIE","TYR","DEN"],"ITALY":["NAP","ROM","ION","VEN","TUN"],"RUSSIA":["WAR","MOS","SEV","STP","RUM","BOT","GAL","FIN","SWE"],"TURKEY":["ANK","SMY","CON","BUL","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["F ALB - TRI","A SER S A GRE - BUL","A TRI - VIE","A BUD S A TRI - VIE"],"ENGLAND":["A YOR H","F NTH - NWG","F NWY S F LON - NTH","F LON - NTH"],"FRANCE":["A MAR H","F MAO - SPA\/SC","A BUR - BEL"],"GERMANY":["F HOL - BEL","A TYR - MUN","A DEN H","A KIE S A MUN - RUH","A MUN - RUH"],"ITALY":["A VEN H","F TUN - WES","F NAP - ION","F ROM - TYS"],"RUSSIA":["F RUM S A GRE - BUL","A GAL S F RUM","A FIN S F SWE - NWY","F SWE - NWY","A SEV - UKR","F STP\/NC S F SWE - NWY"],"TURKEY":["A SMY H","F CON - BUL\/SC","A GRE S F CON - BUL","F ANK - CON"]},"results":{"A SER":["void"],"F ALB":[],"A TRI":[],"A BUD":[],"F NTH":[],"A YOR":[],"F NWY":["cut","dislodged"],"F LON":[],"A MAR":[],"F MAO":[],"A BUR":["bounce"],"F HOL":["bounce"],"A TYR":[],"A DEN":[],"A KIE":[],"A MUN":[],"A VEN":[],"F TUN":[],"F NAP":[],"F ROM":[],"F RUM":["void"],"A GAL":[],"A FIN":[],"F SWE":[],"A SEV":[],"F STP\/NC":[],"A SMY":[],"F CON":[],"A GRE":[],"F ANK":[]},"messages":[{"sender":"FRANCE","recipient":"ENGLAND","time_sent":724354,"phase":"S1902M","message":"Too good to be true. I wish I could tell you that earlier... Also, I REALLY did not want to see you build a fleet in London. This will create quite a bit of friction between us..."},{"sender":"FRANCE","recipient":"GERMANY","time_sent":724398,"phase":"S1902M","message":"I suppose I should protect myself from your two new armies."},{"sender":"FRANCE","recipient":"AUSTRIA","time_sent":724431,"phase":"S1902M","message":"Oh... how is she my nemesis? Other than the fact that she just built two fleets."},{"sender":"FRANCE","recipient":"ITALY","time_sent":724505,"phase":"S1902M","message":"Ok. Well now I think I know what is happening... Those three fleets of yours look awfully menacing. I do not think you can get to me fast enough to get a real foothold on Iberia, but you will certainly leave us stagnant if you try."},{"sender":"FRANCE","recipient":"TURKEY","time_sent":724534,"phase":"S1902M","message":"You need to work with Russia. Italy and Austria are allied."},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":724637,"phase":"S1902M","message":"Ok, well it seems our interests will be coinciding. Italy and Austria are allied, so it would be a good idea if you help out Turkey. I do not know if England and Germany are allied, but obviously their builds are worrying to me. I understand you managed to dissuade England from building with Edi. Scoundrel. =P"},{"sender":"TURKEY","recipient":"FRANCE","time_sent":726427,"phase":"S1902M","message":"This is what I've determined."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":729318,"phase":"S1902M","message":"Surprisingly he was easily convinced. I do not think Germany and England are allied...but was surprised by no fleet build by Germany. I guess he will leave it up to me to go against England."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":730438,"phase":"S1902M","message":"Austria's campaigning me pretty hard to go after you. This obviously isn't a great idea, since he's allied with Italy. I'm a bit concerned about your army in Sev, but it's no fleet, which is good. What are your plans there?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":736697,"phase":"S1902M","message":"Yeah... Don't sweat the army. My fleet is useless in RUM... All I can do is hold BUL. I need to get the fleet back to SEV and the army to RUM so that it can become an offensive weapon. If you want to devise a plan to disband the fleet, I am OK with that. Italy has told me she is allied with Austria now...or least she has made peace. That can only mean that she intends to control the Med.<br \/><br \/>If you look at my perspective, I built North to face England...and I had to build South to face Austria...it seems everyone understands that Austria and I are at war (again this would leave you open to Italy which I think is Austria's intent. Anyway, I couldn't build in Warsaw due to an agreement to obtain Sweden. And Moscow just puts me farther from the front. A fleet would be aggressive toward you ... Hence the army in SEV."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":738721,"phase":"S1902M","message":"I knew it would be I'm moving North Sea to Norwegian Sea and London to North Sea, has almost the same effect as building in Edinburgh."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":762398,"phase":"S1902M","message":"Thanks for the explanation. I don't see any way to force Rum to disband soon, without me moving to the Black Sea. Not exactly the best plan, if you ask me! I'd like to keep the Black Sea neutral, so perhaps you can move Sev - Ukr; Rum - Sev? Even that isn't a great plan. <br \/><br \/>Austria's probably coming hard towards Rum, Bul and Gre. I can't hold Gre this turn, so I'm planning on just using Gre to take Bul. I'll hit the Aegean, then I can see about taking Gre back in the fall."},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":763196,"phase":"S1902M","message":"Well, I was hinting: we played the Three Fleets opening. It is a good way to attack France. You though gave her the gift of an NMR, so it is even easier for her now. Maybe I should stab her to try to stop things getting out of control..."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":765586,"phase":"S1902M","message":"Yes...I agree with your assessment."},{"sender":"ITALY","recipient":"FRANCE","time_sent":767960,"phase":"S1902M","message":"this may well be true."},{"sender":"ITALY","recipient":"TURKEY","time_sent":768035,"phase":"S1902M","message":"so what is your plan with that fleet? it won't help much against austria."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":782697,"phase":"S1902M","message":"Wow, you dodged a bullet there. Would you care to support Tyr-Ven?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":788203,"phase":"S1902M","message":"so what are you thinking for this turn? i believe i am headed west."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":788227,"phase":"S1902M","message":"what are you thinking for this turn?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":788242,"phase":"S1902M","message":"alright, what is your plan for this turn?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":788261,"phase":"S1902M","message":"i'm headed towards france. and you? that russian fleet can't be good for you."},{"sender":"FRANCE","recipient":"ITALY","time_sent":796006,"phase":"S1902M","message":"But you will try it anyway. I have information that would make you reconsider this course of action, but I can't decide if I'm better off telling you with the good chance you'll tell him I told you, or if I should let you attack me and keep him as a friend."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":797790,"phase":"S1902M","message":"I knew that would happen, I'm not headed towards France because this Russian German alliance wont be good for anyone. I built in London because if he built in St. P which he did Edi or Lon wouldn't matter and he made a treaty to not build in Edi and he wouldn't build in St. P I knew he would but again, wouldn't change the outcome. I intend on moving london into the North Sea and North Sea up."},{"sender":"TURKEY","recipient":"ITALY","time_sent":800360,"phase":"S1902M","message":"It does against Russia."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":800472,"phase":"S1902M","message":"I would suggest:<br \/><br \/>Rum Hold<br \/>Sev - Ukr<br \/>Gal S Rum Hold<br \/><br \/>Keeps Rum secure, but gives you the ability to swap the army in to Rum in Fall. What do you think?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":800623,"phase":"S1902M","message":"I thought you wanted RUM to support you to BUL?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":800709,"phase":"S1902M","message":"I am trying to get Turkey to help with Russia. Germany has asked me to support Tyr into Venice."},{"sender":"ITALY","recipient":"TURKEY","time_sent":803306,"phase":"S1902M","message":"lol indeed. so have you made peace with austria, then?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":803372,"phase":"S1902M","message":"alright, sounds good. except the tyr-ven part =)<br \/><br \/>he also offered (if i had retreated to bud) to help me attack you. so, he's just being opportunistic. hope you don't go for it?<br \/><br \/>turkey said he was moving against russia, and i was surprised! well done =)"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":803417,"phase":"S1902M","message":"so i'm on my own against france, you say?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":803426,"phase":"S1902M","message":"tell! tell!"},{"sender":"ITALY","recipient":"FRANCE","time_sent":803480,"phase":"S1902M","message":"look, it seems england is not going to attack you. if that is the case, you are right, it's a waste for me to go for you. which leaves me in a bit of a lurch...."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":803722,"phase":"S1902M","message":"Turkey seems pleased that you were moving West. That was the key. AS for Tyrolia, should we push Germany back? I will move for Vienna this turn and fill in Trieste with my fleet, then we can hit Tyrolia together."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":804715,"phase":"S1902M","message":"yeah, the only problem is then he can retreat to pie. but oh well. just have to take that risk. the problem is that now england is not attacking france."},{"sender":"ITALY","recipient":"GERMANY","time_sent":804823,"phase":"S1902M","message":"alright, herr mapleleaf. england is moving against russia, leaving the spoils of france to us. you can begin by taking bel, and leaving tyr finally. then we can coordinate against france, what do you think?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":804850,"phase":"S1902M","message":"also, look out b\/c england mentioned to me a german-russian alliance, so you probably should support yourself into vie."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":805175,"phase":"S1902M","message":"Oh yes, silly me overlooking that."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":805189,"phase":"S1902M","message":"Gre to Bul, support from Rum. Everything else is a-ok."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":810715,"phase":"S1902M","message":"If it means giving Germany and Russia an even bigger advantage then yes."},{"sender":"FRANCE","recipient":"ITALY","time_sent":812934,"phase":"S1902M","message":"Fleets are always useful. No matter where you throw them. =)"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":812956,"phase":"S1902M","message":"Ok. Have you managed to recruit Germany to help you with Russia?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":822537,"phase":"S1902M","message":"I will (Vienna move...). Thanks."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":822577,"phase":"S1902M","message":"I'm hearing reports of a solid Russian\/German pact. We really need to get more units on."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":847326,"phase":"S1902M","message":"I'm certain they are working together."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":848600,"phase":"S1902M","message":"Interesting. Very much unexpected."},{"sender":"TURKEY","recipient":"ITALY","time_sent":848872,"phase":"S1902M","message":"One does well with allies in this game, or so I've heard."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":848958,"phase":"S1902M","message":"I think I've convinced Italy that I'm moving on you."},{"sender":"ITALY","recipient":"TURKEY","time_sent":851609,"phase":"S1902M","message":"haha, funny thing, i've heard that too =)"},{"sender":"ITALY","recipient":"TURKEY","time_sent":855194,"phase":"S1902M","message":"i haven't heard a damn thing from the german, have you?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":855215,"phase":"S1902M","message":"i haven't heard anything from germany, have you?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":855283,"phase":"S1902M","message":"how are things? i hear austria is now gunning for you, or else it's a good fakeout. how are your relations with england and germany? england said he's after you, which leaves me alone against france, i guess."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":855303,"phase":"S1902M","message":"sorry, what does that mean?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":855360,"phase":"S1902M","message":"Well. That is not good at all, for either of us."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":856237,"phase":"S1902M","message":"I thought I had replied to you...sorry. Well, I did move to Galicia...so I am not surprised he is angry. Sorry that you have decided not to work with me in the East. Good luck in the Western Sphere."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":856299,"phase":"S1902M","message":"well, i'll be coming back shortly, don't worry. <br \/><br \/>and i didn't mean austria is gunning for you, that's a forgone conclusion, i meant turkey, my mistake."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":856332,"phase":"S1902M","message":"tho i don't like to see russia go down quickly, so if it looks like you're in danger, i'll be coming over sooner rather than later."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":856611,"phase":"S1902M","message":"Not since he asked me to support him into Venice. That was some time ago."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":856764,"phase":"S1902M","message":"Serbia is supporting Greece into Bulgaria. It has to be the army - a fleet on the South Coast of Bul is really limited. My own fleet is moving to Trieste (hopefully)."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":856890,"phase":"S1902M","message":"well that's a shame. this game is much less press-intensive than i had hoped it would be."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":856945,"phase":"S1902M","message":"I think things will liven up, especially when your own plan becomes clear to the others."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":857192,"phase":"S1902M","message":"oh my plan is pretty clear, i think. lol"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":872068,"phase":"S1902M","message":"Germany and Russia have shown me that they are working together and I can't take them alone let alone take on a Friendly nation."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":872133,"phase":"S1902M","message":"I know, I'm hoping Turkey and Austria work together to slow them but Italy seems to have turned on a whim."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":875521,"phase":"S1902M","message":"oh i see. i didn't realize they were working together."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":875534,"phase":"S1902M","message":"alright, i'll work on france by my lonesome..."},{"sender":"GERMANY","recipient":"ITALY","time_sent":877051,"phase":"S1902M","message":"I am fine with it."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":877099,"phase":"S1902M","message":"I'll take that as a no."},{"sender":"ITALY","recipient":"GERMANY","time_sent":877572,"phase":"S1902M","message":"what are you doing with tyr?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":883261,"phase":"S1902M","message":"No, Italy and Austria are and have been working together."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":888958,"phase":"S1902M","message":"I cannot afford to do so right now, I have to deal with the Russians."},{"sender":"TURKEY","recipient":"ITALY","time_sent":893258,"phase":"S1902M","message":"No, not really. He's playing pretty tight to the chest this game...<br \/>I've played with him before and he was a bit more talkative."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":893278,"phase":"S1902M","message":"Excellent."},{"sender":"ITALY","recipient":"TURKEY","time_sent":893282,"phase":"S1902M","message":"hm. well, such is life i guess."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":893295,"phase":"S1902M","message":"We can look at taking Rum in Fall then."},{"sender":"TURKEY","recipient":"ITALY","time_sent":893327,"phase":"S1902M","message":"I'd assume he's hostile.<br \/><br \/>Even if he was talking to me. :)"},{"sender":"ITALY","recipient":"TURKEY","time_sent":893622,"phase":"S1902M","message":"yes, that's my assumption as well, lol."}]},{"name":"S1902R","state":{"timestamp":1537459322946191,"zobrist_hash":"8674067130586815139","note":"","name":"S1902R","units":{"AUSTRIA":["A SER","A BUD","F TRI","A VIE"],"ENGLAND":["A YOR","F NWG","F NTH","*F NWY"],"FRANCE":["A MAR","A BUR","F SPA\/SC"],"GERMANY":["F HOL","A DEN","A KIE","A MUN","A RUH"],"ITALY":["A VEN","F WES","F ION","F TYS"],"RUSSIA":["F RUM","A GAL","A FIN","F STP\/NC","F NWY","A UKR"],"TURKEY":["A SMY","A GRE","F BUL\/SC","F CON"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","ALB","TRI","VIE"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","NTH"],"FRANCE":["BRE","MAR","PAR","MAO","BUR","SPA"],"GERMANY":["BER","HOL","KIE","TYR","DEN","MUN","RUH"],"ITALY":["NAP","ROM","VEN","TUN","WES","ION","TYS"],"RUSSIA":["WAR","MOS","SEV","STP","RUM","BOT","GAL","FIN","SWE","NWY","UKR"],"TURKEY":["ANK","SMY","GRE","BUL","CON"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NWY R SKA"],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"F NWY":[]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":898791,"phase":"S1902R","message":"hm, turkey's moves don't look very anti-russian...<br \/><br \/>are you concerned at all? maybe i should leave a fleet at ion?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":898878,"phase":"S1902R","message":"well, i guess i get a pass for now. <br \/><br \/>your moves don't look very anti-russian after all. i would have thought ank-bla. care to explain?"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":903338,"phase":"S1902R","message":"oh my...totally forgot to change my orders.<br \/><br \/>At least it worked out."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":911337,"phase":"S1902R","message":"I am not too concerned - I let him take Bul so that he can build and get at Russia. It helps me because Russia is distracted"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":911395,"phase":"S1902R","message":"The German moves are good - no pressure on Venice or Vienna now. The really good part is that just about everyone else is changing plans. That usually means lost time."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":911441,"phase":"S1902R","message":"Yes, I hope so. Note that Russia did take Norway - he must not keep Rum or he builds."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":934782,"phase":"S1902R","message":"yes, it's true. the german won't even speak to me, but as long as he stays out of tyr and boh, i'll be ok with that."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":935511,"phase":"S1902R","message":"Juggernaut obviously still forming and you still don't help?You are being moved on but we still have time to put Russia in his place."}]},{"name":"F1902M","state":{"timestamp":1537459322956002,"zobrist_hash":"1222567013287870074","note":"","name":"F1902M","units":{"AUSTRIA":["A SER","A BUD","F TRI","A VIE"],"ENGLAND":["A YOR","F NWG","F NTH","F SKA"],"FRANCE":["A MAR","A BUR","F SPA\/SC"],"GERMANY":["F HOL","A DEN","A KIE","A MUN","A RUH"],"ITALY":["A VEN","F WES","F ION","F TYS"],"RUSSIA":["F RUM","A GAL","A FIN","F STP\/NC","F NWY","A UKR"],"TURKEY":["A SMY","A GRE","F BUL\/SC","F CON"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","ALB","TRI","VIE"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","NTH","SKA"],"FRANCE":["BRE","MAR","PAR","MAO","BUR","SPA"],"GERMANY":["BER","HOL","KIE","TYR","DEN","MUN","RUH"],"ITALY":["NAP","ROM","VEN","TUN","WES","ION","TYS"],"RUSSIA":["WAR","MOS","SEV","STP","RUM","BOT","GAL","FIN","SWE","NWY","UKR"],"TURKEY":["ANK","SMY","GRE","BUL","CON"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A SER H","A BUD S A VIE - GAL","F TRI H","A VIE - GAL"],"ENGLAND":["A YOR - NWY VIA","F NTH C A YOR - NWY","F NWG S A YOR - NWY","F SKA S A YOR - NWY"],"FRANCE":["A MAR H","A BUR H","F SPA\/SC H"],"GERMANY":["F HOL S A RUH - BEL","A DEN - SWE","A KIE - DEN","A RUH - BEL","A MUN - BUR"],"ITALY":["A VEN - PIE","F WES - MAO","F ION - TYS","F TYS - LYO"],"RUSSIA":["F RUM S F BUL\/SC","A GAL S F RUM","A FIN - SWE","F STP\/NC S F NWY","A UKR S F RUM","F NWY S A FIN - SWE"],"TURKEY":["A SMY H","A GRE S F BUL\/SC","F BUL\/SC S F CON - AEG","F CON - AEG"]},"results":{"A SER":[],"A BUD":[],"F TRI":[],"A VIE":[],"A YOR":[],"F NWG":[],"F NTH":[],"F SKA":[],"A MAR":[],"A BUR":[],"F SPA\/SC":[],"F HOL":[],"A DEN":["bounce"],"A KIE":["bounce"],"A MUN":["bounce"],"A RUH":[],"A VEN":[],"F WES":[],"F ION":[],"F TYS":[],"F RUM":[],"A GAL":["cut","dislodged"],"A FIN":["bounce"],"F STP\/NC":[],"F NWY":["cut","dislodged"],"A UKR":[],"A SMY":[],"A GRE":[],"F BUL\/SC":[],"F CON":[]},"messages":[{"sender":"ENGLAND","recipient":"GERMANY","time_sent":937047,"phase":"F1902M","message":"Sweden is completely free to you via Denmark to Sweden.<br \/><br \/>I am supporting North Sea to Norway with 3 so he can't defend Sweden and hold Norway, either you gain or I regain. It is essential to slow this power down and I\"ll be focused on Russia, you also seem to have Italy helping you with France.<br \/><br \/>I just know I can't slow this Russia on my own and wouldn't mind support from a fellow neighbor."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":953562,"phase":"F1902M","message":"Are you in a position to go after England? I am willing to keep moving NOR to the North Sea or attempt to support you to the NS."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1032342,"phase":"F1902M","message":"I will consider this option. Thank you for the information."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":1032373,"phase":"F1902M","message":"Fair enough. As you can see, I am focusing elsewhere."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1032521,"phase":"F1902M","message":"You will need to ally with me versus France though."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1032704,"phase":"F1902M","message":"Why don't you convoy Yor-Nor? You need to commit to the north."},{"sender":"FRANCE","recipient":"ITALY","time_sent":1041308,"phase":"F1902M","message":"It would be helpful for both of us if you turned your fleets around now, so that I don't have to commit builds to fending you off."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":1041334,"phase":"F1902M","message":"I can support you into Belgium."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":1041399,"phase":"F1902M","message":"Actually, the best tactical scenarios occur if you support me into Belgium. Are you ok with that or are you going to try to get Norway back?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1041431,"phase":"F1902M","message":"Especially considering it looks like we have a Juggernaut on our hands."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1046391,"phase":"F1902M","message":"everything i've heard is to the contrary, but you might be right. a juggernaut is a field day for you, in any case, no?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1051467,"phase":"F1902M","message":"Really? Who has been saying to the contrary? I think it's quite obvious that you and Austria have something going, and from the moves and what I would guess is the natural reaction to a perceived AI alliance is a Juggernaut."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":1057798,"phase":"F1902M","message":"Did you have ideas for this turn?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1064777,"phase":"F1902M","message":"ok i haven't heard much from you this turn. should i be worried?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1064929,"phase":"F1902M","message":"Not at all. I have been busy - and I am in a different time zone, so access is not well matched with you.<br \/><br \/>The German moves are very helpful as he is actually putting pressure on France."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1065004,"phase":"F1902M","message":"I have not decided my exact moves yet but I am trying to get Turkey to just go north. To get a build, I will probably launch at warsaw and turn against Germany."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1065163,"phase":"F1902M","message":"We should get the second game up soon, and that one is supposed to give us the country we get twice (Game 8 as well). What would you prefer?<br \/><br \/>I am still pleased that I got Austria out of the way here... I would like to avoid Germany - I have played that in seven out of ten games recently - three out of four in the League Group and it has been very difficult because of meta-gaming. I would be happy with any other country."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1072813,"phase":"F1902M","message":"i guess i am worried about a jugglenaut forming after all. but i'll go with the planned moves for this turn, and then take it from there."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1078986,"phase":"F1902M","message":"I think Austria is going to be looking to take Rum. Not sure though."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1079219,"phase":"F1902M","message":"I suspected that as well."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1080009,"phase":"F1902M","message":"Jugglenaut? That's a new one... ; )"},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":1093539,"phase":"F1902M","message":"You have the fleet problem I mentioned earlier. You need to get an army into Bul."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1093578,"phase":"F1902M","message":"Pushing Galicia back, I hope..."}]},{"name":"F1902R","state":{"timestamp":1537459322958203,"zobrist_hash":"1829118797948735689","note":"","name":"F1902R","units":{"AUSTRIA":["A SER","A BUD","F TRI","A GAL"],"ENGLAND":["F NWG","F NTH","F SKA","A NWY"],"FRANCE":["A MAR","A BUR","F SPA\/SC"],"GERMANY":["F HOL","A DEN","A KIE","A MUN","A BEL"],"ITALY":["A PIE","F MAO","F TYS","F LYO"],"RUSSIA":["F RUM","A FIN","F STP\/NC","A UKR","*A GAL","*F NWY"],"TURKEY":["A SMY","A GRE","F BUL\/SC","F AEG"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","ALB","TRI","VIE","GAL"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","NTH","SKA","NWY"],"FRANCE":["BRE","MAR","PAR","BUR","SPA"],"GERMANY":["BER","HOL","KIE","TYR","DEN","MUN","RUH","BEL"],"ITALY":["NAP","ROM","VEN","TUN","WES","ION","PIE","MAO","TYS","LYO"],"RUSSIA":["WAR","MOS","SEV","STP","RUM","BOT","FIN","SWE","UKR"],"TURKEY":["ANK","SMY","GRE","BUL","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["A GAL R WAR","F NWY R BAR"],"TURKEY":[]},"results":{"A GAL":[],"F NWY":[]},"messages":[{"sender":"FRANCE","recipient":"GLOBAL","time_sent":1126116,"phase":"F1902R","message":"I could have sworn I had more time to enter moves..."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1132665,"phase":"F1902R","message":"yup, looks like a juggle alright. dammit."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":1133119,"phase":"F1902R","message":"Thank you for attacking Sweden I will help you attack France as well. Italy seems to have a higher option for SC's so we cannot delay. Let me know how we can coordinate."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":1148817,"phase":"F1902R","message":"Are you changing your mind here and going after A & I? I hope not."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":1198501,"phase":"F1902R","message":"ouch"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1198522,"phase":"F1902R","message":"would you mind to send tri-alb, and we can keep ven\/tri dmz?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1198565,"phase":"F1902R","message":"it might be needed to keep turkey from advancing his fleets farther, too."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1198586,"phase":"F1902R","message":"ok so what's your plan here? still attacking russia? ;D"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1198690,"phase":"F1902R","message":"going with a good ol' jugglenaut, eh?<br \/><br \/>fair enough. i can hold off a turkey for a bit, you grow yourself some, once i get a build i can push turkey back, and then we can finish him off together?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":1198760,"phase":"F1902R","message":"what are your thoughts about the jugglenaut? i guess it doesn't concern you too much since you are not being attacked by russia? i think i'd like to take spain and then focus on turkey. what do you think?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1198872,"phase":"F1902R","message":"it looks like we've got a jugglenaut on our hands, so i'm glad you've kept the pressure on russia. what is up with germany? random, or what? i am going to get my build from france and then work on turkey, who is getting quite big already. let me know if you hear anything interesting from him and i'll do the same with russia."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1198915,"phase":"F1902R","message":"and it seems you are right. well, i guess i've come all this way, i might as well finish what i started - i'll need to a build to fight turkey in any case."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":1201239,"phase":"F1902R","message":"Okay, I've done enough Juggernauts to sense one. Germany is the most indecisive player I've played with lol. I'm hoping he helps me keeps Russia back. I showed him how obvious the Juggernaut is and reminded him that Germany is more affected before England."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1201895,"phase":"F1902R","message":"Yes. The jugg is looking insurmountable, but the least we can do is fight it."},{"sender":"GERMANY","recipient":"ITALY","time_sent":1201956,"phase":"F1902R","message":"As you can see, I just attempted to stab Russia. We should dispose of France quickly, then work on the jugg."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1201993,"phase":"F1902R","message":"I was thinking about Trieste to the Adr, but I appreciate the need for breathing space. I will move - or at least bounce Turkey. You really do need to relax with me: I only stab when it makes a significant difference. To do so now would be plain stupid. I would love to see us get a draw from this one as we had relatively weak nations to start with."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":1202011,"phase":"F1902R","message":"We have time to figure something out."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1202100,"phase":"F1902R","message":"that works. i am not too worried about a stab, since it would indeed be pointless. mostly i'd like to see the fleet at tri get into action against the turk. i'll be sending tys-ion with the hopes of a bounce - if you are at alb or adr, then you can support me into ion next turn?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":1202134,"phase":"F1902R","message":"i was trying to figure out what happened up there! so that's it. great.<br \/><br \/>about france, sounds perfect."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1202188,"phase":"F1902R","message":"it seems like germany is inclined to move against russia. between his moves and his press, at least. have you heard anything from him lately?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1202545,"phase":"F1902R","message":"Yes. The reason I mentioned Adr is because that move is assured. However, I don't want Turkey in Albania... Hmmm, hard choice here. The fleet will be ordered to move, not sure which move yet though."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1202611,"phase":"F1902R","message":"alright, sounds good. if things go according to plan i should get a build this year which i can dedicate against turkey."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1202744,"phase":"F1902R","message":"Okay. I can go for Rumania because of that silly Turkish fleet in Bulgaria."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1202804,"phase":"F1902R","message":"yes, that's great =)"}]},{"name":"W1902A","state":{"timestamp":1537459322959900,"zobrist_hash":"3040030557013007452","note":"","name":"W1902A","units":{"AUSTRIA":["A SER","A BUD","F TRI","A GAL"],"ENGLAND":["F NWG","F NTH","F SKA","A NWY"],"FRANCE":["A MAR","A BUR","F SPA\/SC"],"GERMANY":["F HOL","A DEN","A KIE","A MUN","A BEL"],"ITALY":["A PIE","F MAO","F TYS","F LYO"],"RUSSIA":["F RUM","A FIN","F STP\/NC","A UKR","A WAR","F BAR"],"TURKEY":["A SMY","A GRE","F BUL\/SC","F AEG"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","GRE","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","ALB","TRI","VIE","GAL"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","NTH","SKA","NWY"],"FRANCE":["BRE","MAR","PAR","BUR","SPA"],"GERMANY":["BER","HOL","KIE","TYR","DEN","MUN","RUH","BEL"],"ITALY":["NAP","ROM","VEN","TUN","WES","ION","PIE","MAO","TYS","LYO"],"RUSSIA":["MOS","SEV","STP","RUM","BOT","FIN","SWE","UKR","WAR","BAR"],"TURKEY":["ANK","SMY","GRE","BUL","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":1,"homes":["BRE","PAR"]},"GERMANY":{"count":1,"homes":["BER"]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":1,"homes":["ANK","CON"]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["A BRE B"],"GERMANY":["F BER B"],"ITALY":[],"RUSSIA":[],"TURKEY":["A CON B"]},"results":{"A BRE":[""],"F BER":[""],"A CON":[""]},"messages":[{"sender":"ENGLAND","recipient":"ITALY","time_sent":1208202,"phase":"W1902A","message":"Only that he'll consider hitting Sweden after I showed his advancement to his north was not to his advantage."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":1208263,"phase":"W1902A","message":"If we start now we'll have nothing to worry about and even be in a position to push or take France out while we hold."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1208292,"phase":"W1902A","message":"well, hit it he did, no?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":1208377,"phase":"W1902A","message":"Then then I guess that was the most recent :P<br \/><br \/>Bit of Yoda grammar in that statement."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1208420,"phase":"W1902A","message":"lol you're right =D"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1211325,"phase":"W1902A","message":"Can I suggest you go farther north for said build?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1211516,"phase":"W1902A","message":"really? from england? who is actually doing something about the jugglenaut? sorry, but i can't do that."},{"sender":"FRANCE","recipient":"ITALY","time_sent":1211820,"phase":"W1902A","message":"Yes, but the Juggernaut doesn't mean that everyone has to drop what they're doing and run at them. England and Germany have much to gain in Russia's North, and I don't see you and Austria making much progress in Turkey for a while.<br \/><br \/>If you take one of my centres, you can rest assured that with nothing else to do with my units, I will be moving to take it back. I don't want to do this, especially if it forces you to keep units in this area, which could be fighting the Juggernaut. I don't think it's worth it for you to keep like three units over here to keep one SC. Do you?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1211962,"phase":"W1902A","message":"all good points.<br \/><br \/>so should i drop everything to fight turkey, or not?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1212014,"phase":"W1902A","message":"j\/k<br \/><br \/>but seriously, what else would you have me do? i won't make much progress against turkey anyway, and i'm already over here. i hope my three units won't just support one SC, but hopefully more. who knows."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1212053,"phase":"W1902A","message":"why not let me have spain and focus on defending against germany? you can keep portugal, and i can back off a bit. then we'd all be happy?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1215693,"phase":"W1902A","message":"I'll do the deal if I see you back off more than a bit."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1215723,"phase":"W1902A","message":"what does that mean?"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1234390,"phase":"W1902A","message":"I'm unsure as to how you'd like to continue.<br \/><br \/>You can put significant pressure on Gal this season. Not sure that you'd be able to dislodge him there.<br \/><br \/>I'm going to try and get an Army in Bul, just not sure what my plan will be on that one. Any thoughts?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":1234433,"phase":"W1902A","message":"Pure madness! Absolute insanity!<br \/><br \/>In all fairness, I don't think my plans should be shared with you. I hear you're working with someone."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1234950,"phase":"W1902A","message":"i'm on my own, a lone wolf!"},{"sender":"TURKEY","recipient":"ITALY","time_sent":1235131,"phase":"W1902A","message":"Me too!"},{"sender":"ITALY","recipient":"TURKEY","time_sent":1235904,"phase":"W1902A","message":"lol<br \/><br \/>alright, so let's team up. what would you like to do this turn?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":1240174,"phase":"W1902A","message":"I think I'm going to build a unit."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1241591,"phase":"W1902A","message":"no... you've got to be kidding! are you sure you don't want to rethink that?<br \/><br \/>or, if you insist, build an army at ankara. so you can knock off russia...."},{"sender":"FRANCE","recipient":"ITALY","time_sent":1246678,"phase":"W1902A","message":"I'm saying that I'm ok with you in Spain if you back off back behind the DMZ's. You can leave a unit in Spain, I'll leave one in Marseilles with the simple order of move to Spain to cover Portugal."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1290991,"phase":"W1902A","message":"and what happens to the unit at spain?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1322714,"phase":"W1902A","message":"My unit or your unit? My fleet will likely go to Marseilles, and yours can stay in Spain."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1324874,"phase":"W1902A","message":"so you won't move spc (sc) to por? that's what i meant, sorry if i was unclear. i assumed you would take por, and still have an army in mar, leaving me with two on one."},{"sender":"FRANCE","recipient":"ITALY","time_sent":1332213,"phase":"W1902A","message":"Oh right. Forgot about that. Ok, well then forget the unit in Marseilles."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1333902,"phase":"W1902A","message":"so where would that unit go? sorry, i am being so particular. hopefully it shows to you that i am seriously considering it. otherwise i wouldn't bother asking such detailed questions."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1334005,"phase":"W1902A","message":"hey no word? i like it better when we chat."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1334072,"phase":"W1902A","message":"i'm not really working against you, just not with you either. we could still share info - i mean, probs not about direct enemies, but maybe about others? like what are you hearing from england and germany?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1334132,"phase":"W1902A","message":"haven't heard from you in a bit. obviously i'm working with austria-hungary for now, but that doesn't mean it will be that way for ever. what are you looking at for a couple turns down the line? it looks like some set backs for you, for sure, no?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":1334230,"phase":"W1902A","message":"what's your plan for this turn? you have a build, i'm thinking it will be a fleet? as of right now, my plan is to hit mar and take spa. but if there's a better approach, let me know..."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1334248,"phase":"W1902A","message":"anything new? everything's pretty quiet still for me."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":1335376,"phase":"W1902A","message":"You could send your fleets and ravage the English. :)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1335525,"phase":"W1902A","message":"yea, you are not the first to suggest that haha"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1337424,"phase":"W1902A","message":"To fight Germany. I can't say for sure because it depends on the actions of others."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1337769,"phase":"W1902A","message":"Nothing yet..."},{"sender":"TURKEY","recipient":"ITALY","time_sent":1367469,"phase":"W1902A","message":"I haven't spoken to England since the game opened and I find it unlikely that he'll share any information with me.<br \/><br \/>Germany is quiet. I suspect he's made deals with Russia to secure that front while he takes his bite out of France."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1371303,"phase":"W1902A","message":"strange. normally england and turkey ought to talk a lot, no? <br \/><br \/>everyone seems to be pretty quiet. weird game."},{"sender":"GERMANY","recipient":"ITALY","time_sent":1372071,"phase":"W1902A","message":"I need a fleet"},{"sender":"ITALY","recipient":"GERMANY","time_sent":1372267,"phase":"W1902A","message":"yeah, that's what i was thinking. a good time to make one, too - you can justify needing it against russia, but it will com in handy later against england ;D<br \/><br \/>are you going to take bur or try to slip past into picardy?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":1373286,"phase":"W1902A","message":"Stay tuned...."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1374285,"phase":"W1902A","message":"alright."}]},{"name":"S1903M","state":{"timestamp":1537459322971211,"zobrist_hash":"6748361367673955015","note":"","name":"S1903M","units":{"AUSTRIA":["A SER","A BUD","F TRI","A GAL"],"ENGLAND":["F NWG","F NTH","F SKA","A NWY"],"FRANCE":["A MAR","A BUR","F SPA\/SC","A BRE"],"GERMANY":["F HOL","A DEN","A KIE","A MUN","A BEL","F BER"],"ITALY":["A PIE","F MAO","F TYS","F LYO"],"RUSSIA":["F RUM","A FIN","F STP\/NC","A UKR","A WAR","F BAR"],"TURKEY":["A SMY","A GRE","F BUL\/SC","F AEG","A CON"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","GRE","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","ALB","TRI","VIE","GAL"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","NTH","SKA","NWY"],"FRANCE":["BRE","MAR","PAR","BUR","SPA"],"GERMANY":["BER","HOL","KIE","TYR","DEN","MUN","RUH","BEL"],"ITALY":["NAP","ROM","VEN","TUN","WES","ION","PIE","MAO","TYS","LYO"],"RUSSIA":["MOS","SEV","STP","RUM","BOT","FIN","SWE","UKR","WAR","BAR"],"TURKEY":["ANK","SMY","GRE","BUL","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A SER H","A BUD S A GAL","F TRI - ALB","A GAL H"],"ENGLAND":["F NTH - ENG","F NWG S A NWY","F SKA S A DEN - SWE","A NWY S A DEN - SWE"],"FRANCE":["A MAR H","A BUR S A MAR","F SPA\/SC - POR","A BRE H"],"GERMANY":["F HOL - HEL","A DEN - SWE","A KIE - HOL","A MUN - BUR","A BEL S A MUN - BUR","F BER - BAL"],"ITALY":["A PIE - MAR","F MAO - SPA\/SC","F LYO S A PIE - MAR","F TYS - ION"],"RUSSIA":["F RUM H","A FIN S A DEN - SWE","F STP\/NC - NWY","A UKR S F RUM","F BAR S F STP\/NC - NWY","A WAR - GAL"],"TURKEY":["A SMY - CON","A GRE - ALB","F BUL\/SC - GRE","F AEG S A CON - BUL","A CON - BUL"]},"results":{"A SER":[],"A BUD":[],"F TRI":["bounce"],"A GAL":[],"F NWG":[],"F NTH":[],"F SKA":[],"A NWY":["cut"],"A MAR":["dislodged"],"A BUR":["cut","dislodged"],"F SPA\/SC":[],"A BRE":[],"F HOL":[],"A DEN":[],"A KIE":[],"A MUN":[],"A BEL":[],"F BER":[],"A PIE":[],"F MAO":[],"F TYS":[],"F LYO":[],"F RUM":[],"A FIN":[],"F STP\/NC":["bounce"],"A UKR":[],"A WAR":["bounce"],"F BAR":[],"A SMY":["bounce"],"A GRE":["bounce"],"F BUL\/SC":["bounce"],"F AEG":[],"A CON":["bounce"]},"messages":[{"sender":"ITALY","recipient":"FRANCE","time_sent":1385895,"phase":"S1903M","message":"alright that build seems promising. let's see how conversations with others develop."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1391967,"phase":"S1903M","message":"what's your thought with that army? is it headed toward austria? are you going to try for ion? that seems like just a waste of time for us both."},{"sender":"TURKEY","recipient":"ITALY","time_sent":1405318,"phase":"S1903M","message":"Well, I suppose England and Turkey would talk...but England seems to find me unlikeable, due to my alliance of convenience with Russia.<br \/><br \/>Anyway.<br \/><br \/>Army. Austria. Nom Nom Nom."},{"sender":"TURKEY","recipient":"ITALY","time_sent":1405412,"phase":"S1903M","message":"As for Ion, I would like to move there. I won't stay there long, nor will I take any of your centers.<br \/><br \/>I can understand why you might be weary of such a move - but I can assure you, I would prefer to keep things to one opponent."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1411598,"phase":"S1903M","message":"well i can't have you in ion, i'm sorry. as appealing as it sounds to leave you be there so i can go on my merry way with france, i'll have to protect my seas. with a little footwork, you can accomplish the same thing against austria-hungary without going to ion. please consider it."},{"sender":"TURKEY","recipient":"ITALY","time_sent":1414357,"phase":"S1903M","message":"So, what I'm hearing is that no matter what I do, you're moving to Ion, just in case."},{"sender":"TURKEY","recipient":"ENGLAND","time_sent":1427562,"phase":"S1903M","message":"How are things to the north?"},{"sender":"TURKEY","recipient":"GERMANY","time_sent":1427605,"phase":"S1903M","message":"Thank you for the well wishes. You seem to have a good handle on things. Perhaps we shall see each other in Moscow?"},{"sender":"ENGLAND","recipient":"TURKEY","time_sent":1458188,"phase":"S1903M","message":"I'm in a position that is completely worthless so not good at all."},{"sender":"GERMANY","recipient":"TURKEY","time_sent":1462640,"phase":"S1903M","message":"I don't know about that. You and Russia seem to be doing quite well."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1462690,"phase":"S1903M","message":"Will you please support Den-Swe?"},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":1462995,"phase":"S1903M","message":"Any way I can get a player checked?<br \/><br \/>Russia in this one: <br \/><br \/>http:\/\/webdiplomacy.net\/board.php?gameID=12956#gamePanel<br \/><br \/>Odd stats. Only one game, not one single message sent and has played it totally in England's favour up til the last move where he started to absorb units... Looks like a second account to me but I have no tools to check this. I have tried to tempt the 'two' players to attack each other and I think the last move was a cover up. I would appreciate it if this comes within your area of responsibility."},{"sender":"GERMANY","recipient":"ITALY","time_sent":1463238,"phase":"S1903M","message":"You will be able to capture Mar right away, if you order Mid-Spa. I will also capture Bur."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":1463349,"phase":"S1903M","message":"If you support Den-Swe this spring, then I will support you into Nor in the autumn. <br \/>What do you say?"},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":1463530,"phase":"S1903M","message":"The user name is Spanish for a disease (Erythema)... Perhaps he just doesn't speak English?"},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":1463711,"phase":"S1903M","message":"Sorry to be obsessed with this one but I just re-ran the game knowing that Russia never sent a single message. Not very subtle stuff..."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":1464950,"phase":"S1903M","message":"I have it in, I might lose Norway but I'm sure you'll help me retake it at another time. Also, it would be beneficial to keep up our relations."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":1466057,"phase":"S1903M","message":"OK....that sounds fine with me."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1473062,"phase":"S1903M","message":"ok, great, that sounds fantastic to me. then you'll be in good shape to take paris, and then brittany =)"},{"sender":"FRANCE","recipient":"AUSTRIA","time_sent":1478260,"phase":"S1903M","message":"Yes, it seems quite likely that they are multi accounters. I will contact them."},{"sender":"FRANCE","recipient":"ITALY","time_sent":1478489,"phase":"S1903M","message":"Ok, so how do you want to do this? I move to Portugal while you move to Spain from MAO? Will you be backing your other units up?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":1492861,"phase":"S1903M","message":"so, what's Austria doing this turn?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":1494337,"phase":"S1903M","message":"hey, sorry i didn't respond before. yes i think i will move to ion as well. whether i move completely against you and abandon france depends on what you do. <br \/><br \/>i haven't heard austria's plans yet, but i may be able to pass something on to you."},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":1495872,"phase":"S1903M","message":"Thanks."},{"sender":"TURKEY","recipient":"ITALY","time_sent":1498168,"phase":"S1903M","message":"Basically, if I know where he's moving, I can work something out to not move to Ion.<br \/><br \/>If that is something that can be done, I'd like to see you not move there."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1503784,"phase":"S1903M","message":"ok, that should work for us both. let me see what i can learn."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1509587,"phase":"S1903M","message":"I think GRE-ALB would be the best move and sliding BUL-GRE."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1539549,"phase":"S1903M","message":"That's my thought too."},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":1542699,"phase":"S1903M","message":"Just received equally feeble messages from England and Russia in that game... Something seems to have stirred them\/him."},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":1542752,"phase":"S1903M","message":"If the case looks solid, will the game be cancelled? I am barely surviving - but not for long. Others have already suffered."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1548271,"phase":"S1903M","message":"Thank you. I will support you back into Nor, if you lose it."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":1548303,"phase":"S1903M","message":"Great. Thank you."}]},{"name":"S1903R","state":{"timestamp":1537459322973761,"zobrist_hash":"1753279905538652885","note":"","name":"S1903R","units":{"AUSTRIA":["A SER","A BUD","F TRI","A GAL"],"ENGLAND":["F NWG","F SKA","A NWY","F ENG"],"FRANCE":["A BRE","F POR","*A MAR","*A BUR"],"GERMANY":["A BEL","F HEL","A SWE","A HOL","A BUR","F BAL"],"ITALY":["F LYO","A MAR","F SPA\/SC","F ION"],"RUSSIA":["F RUM","A FIN","F STP\/NC","A UKR","A WAR","F BAR"],"TURKEY":["A SMY","A GRE","F BUL\/SC","F AEG","A CON"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","GRE","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","ALB","TRI","VIE","GAL"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","NTH","SKA","NWY","ENG"],"FRANCE":["BRE","PAR","POR"],"GERMANY":["BER","KIE","TYR","DEN","MUN","RUH","BEL","HEL","SWE","HOL","BUR","BAL"],"ITALY":["NAP","ROM","VEN","TUN","WES","PIE","MAO","TYS","LYO","MAR","SPA","ION"],"RUSSIA":["MOS","SEV","STP","RUM","BOT","FIN","UKR","WAR","BAR"],"TURKEY":["ANK","SMY","GRE","BUL","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["A BUR R PAR","A MAR R GAS"],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A MAR":[],"A BUR":[]},"messages":[{"sender":"FRANCE","recipient":"ITALY","time_sent":1568921,"phase":"S1903R","message":"So I guess not. I hope you're watching the balance of power."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1577270,"phase":"S1903R","message":"well the truth is i hadn't decided what to do when you sent that. but then, i got really sick while i'm travelling to this training, so i have been either sick in bed sleeping or at my training, so i didn't get a chance to send more press and update orders like i had hoped to. so, i'm sorry this was the outcome and i wasn't able to make an alternative work, it did seem like our potential agreement was going to be a good one."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1577333,"phase":"S1903R","message":"on the flip side, i can still move back from marseilles, and i am actually on fairly decent terms with turkey, unexpectedly, so i can definitely explore some different options for this turn. i'm about to go to sleep again, but hopefully i'll wake up later and can send some more press."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1577368,"phase":"S1903R","message":"sorry i didn't get back to you - i've been sick and also at a training all day where i couldn't get online."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1577407,"phase":"S1903R","message":"i do appreciate that you didn't move to ion,and i'm willing to explore options for what my ion fleet should do."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1577444,"phase":"S1903R","message":"great, i should be able to get two builds this turn and then help a lot with turkey =)<br \/><br \/>shall we begin by me supporting tri-alb?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1577485,"phase":"S1903R","message":"interesting move to the channel, what are your intentions there?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1579429,"phase":"S1903R","message":"Ok. Get well soon. I'm also sick right now, but it's really mild."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1579469,"phase":"S1903R","message":"thanks."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1580714,"phase":"S1903R","message":"Yes, that makes sense."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":1587610,"phase":"S1903R","message":"France is gone so I figured I might partake."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1588459,"phase":"S1903R","message":"or i could support ser-gre. that could also happen next turn."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1588481,"phase":"S1903R","message":"sounds good! welcome to the feeding frenzy ;D"},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":1598484,"phase":"S1903R","message":"I have checked Russia's profile. He has now sent one message to me but it does not show in the profile. Odd."},{"sender":"TURKEY","recipient":"ITALY","time_sent":1630297,"phase":"S1903R","message":"That is ok.<br \/><br \/>Now you can support me to Alb."},{"sender":"FRANCE","recipient":"AUSTRIA","time_sent":1644625,"phase":"S1903R","message":"I'll see what I can do. At least one of them will likely be banned."},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":1671804,"phase":"S1903R","message":"Can you Keep an eye on the Turkish player as well. He is claiming that Russia and he have been communicating regularly and that he Russian has now told him that he would stop playing and that Turkey should attack him to win."},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":1671813,"phase":"S1903R","message":"Thanks."},{"sender":"AUSTRIA","recipient":"FRANCE","time_sent":1720810,"phase":"S1903R","message":"Any news here? I am running out of time. If Russia does not move, someone will just pick it up as CD. If there has been cheating the game should be stopped. Can you do a Mod pause while this is sorted out?"}]},{"name":"F1903M","state":{"timestamp":1537459322984804,"zobrist_hash":"9089385873415477220","note":"","name":"F1903M","units":{"AUSTRIA":["A SER","A BUD","F TRI","A GAL"],"ENGLAND":["F NWG","F SKA","A NWY","F ENG"],"FRANCE":["A BRE","F POR","A PAR","A GAS"],"GERMANY":["A BEL","F HEL","A SWE","A HOL","A BUR","F BAL"],"ITALY":["F LYO","A MAR","F SPA\/SC","F ION"],"RUSSIA":["F RUM","A FIN","F STP\/NC","A UKR","A WAR","F BAR"],"TURKEY":["A SMY","A GRE","F BUL\/SC","F AEG","A CON"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","GRE","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","ALB","TRI","VIE","GAL"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","NTH","SKA","NWY","ENG"],"FRANCE":["BRE","POR","PAR","GAS"],"GERMANY":["BER","KIE","TYR","DEN","MUN","RUH","BEL","HEL","SWE","HOL","BUR","BAL"],"ITALY":["NAP","ROM","VEN","TUN","WES","PIE","MAO","TYS","LYO","MAR","SPA","ION"],"RUSSIA":["MOS","SEV","STP","RUM","BOT","FIN","UKR","WAR","BAR"],"TURKEY":["ANK","SMY","GRE","BUL","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A SER S A GAL - RUM","A BUD S A GAL - RUM","F TRI - ALB","A GAL - RUM"],"ENGLAND":["F NWG S A NWY","F SKA - NTH","A NWY S A SWE","F ENG S F SKA - NTH"],"FRANCE":["A BRE H","F POR H","A PAR - BUR","A GAS S A PAR - BUR"],"GERMANY":["A BEL S A BUR - PIC","A SWE S F HEL - DEN","A HOL - RUH","F BAL S A SWE","A BUR - PIC","F HEL - DEN"],"ITALY":["F LYO - WES","A MAR S F SPA\/SC","F SPA\/SC S A MAR","F ION S F TRI - ALB"],"RUSSIA":["F RUM H","A FIN S F STP\/NC - NWY","F STP\/NC - NWY","A UKR S F RUM","F BAR S F STP\/NC - NWY","A WAR - GAL"],"TURKEY":["A SMY H","A GRE - SER","F BUL\/SC - CON","F AEG C A CON - BUL","A CON - BUL VIA"]},"results":{"A SER":["cut"],"A BUD":[],"F TRI":[],"A GAL":["bounce"],"F NWG":[],"F SKA":[],"A NWY":["cut","dislodged"],"F ENG":[],"A BRE":[],"F POR":[],"A PAR":[],"A GAS":[],"A BEL":[],"F HEL":[],"A SWE":[],"A HOL":[],"A BUR":[],"F BAL":[],"F LYO":[],"A MAR":[],"F SPA\/SC":[],"F ION":[],"F RUM":[],"A FIN":[],"F STP\/NC":[],"A UKR":[],"A WAR":["bounce"],"F BAR":[],"A SMY":[],"A GRE":["bounce"],"F BUL\/SC":[],"F AEG":[],"A CON":[]},"messages":[{"sender":"TURKEY","recipient":"ITALY","time_sent":1748830,"phase":"F1903M","message":"Any thoughts about Alb?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":1754226,"phase":"F1903M","message":"hm. aside from hurting austria, and making me a new enemy, how does it help me? or is there something you are offering me in exchange?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":1755699,"phase":"F1903M","message":"To be fair, Austria won't be making a move on Ven this season if he suspects me to go for Alb again.<br \/><br \/>You've got two builds from the look of things, so you'll be in a good position to ensure that you can hold your home SCs while I get in a position to move into Austrian home territory.<br \/><br \/>I can make other moves, but I don't think you'll like the looks of them much better."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1755911,"phase":"F1903M","message":"haha i'm sure. yes, it's my decent standing that has my considering it, that's for sure."},{"sender":"TURKEY","recipient":"ITALY","time_sent":1756210,"phase":"F1903M","message":"Though, the more I think about it I may need to rethink my strategy here."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1759899,"phase":"F1903M","message":"ok, well, keep me posted."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1816159,"phase":"F1903M","message":"what now? you're looking in great condition in the north."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1816172,"phase":"F1903M","message":"so what are you thinking?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1816284,"phase":"F1903M","message":"alright, i'm able to focus again on the east. looks like you're having trouble with germany and england. what are you thinking for this turn? for the coming year? i bet austria would be willing to work with you against turkey, if you were game for that. i can help some, but would leave most of turkey's SCs to you, with the idea that once turkey is in the final moments, we can take on austria. or i'm open to other ideas."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1816313,"phase":"F1903M","message":"i'm also pretty sure germany will turn on england, so if you haven't tried that diplomatic track yet, you might want to."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1816366,"phase":"F1903M","message":"ok what are you thinknig for this phase? sorry i'm been a bit quiet, i was sick and out of town. now i'm back, though i have to leave town again on wednesday. i actually might need to get a sitter this time."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1839908,"phase":"F1903M","message":"Not happy about the Greek move to Alb last turn..."},{"sender":"TURKEY","recipient":"ITALY","time_sent":1843080,"phase":"F1903M","message":"Well, I don't need your help in the form of support. Though, it would be nice if you attacked Alb."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":1843791,"phase":"F1903M","message":"You managed to play both sides to your advantage in Sweden. So this will be the turn of reckoning. Are you still intending to support me into Norway?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1844013,"phase":"F1903M","message":"Could Greece move to Serbia? Bulgaria could go to Greece with Aegean support?"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1846534,"phase":"F1903M","message":"Well, there's no way to move to Gre, since I can't get support in."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1848645,"phase":"F1903M","message":"I understand...but if Serbia moves to take Rumania, you can slip in. If he attacks from Galicia, you cut his support. You don't have a shot at a SC this turn...but if you are positioning for next year...I understand."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1885816,"phase":"F1903M","message":"Yes, but you'd have to lose Rum in the process?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1890395,"phase":"F1903M","message":"I'm sorry. I don't understand the question."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1896569,"phase":"F1903M","message":"not terribly surprising, i suppose. i was trying to find it out from T before he did it, but wasn't able to. h originally asked me for support into alb this turn, but now has said he doesn't need it. so, i'm not sure what that means, but i'll let you know if i get anything more."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1896609,"phase":"F1903M","message":"ok so now you want me to order ion-alb? what are you doing with aeg?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":1896935,"phase":"F1903M","message":"I guard against all treachery.<br \/><br \/>I assume StP-Nor. It seems obvious enough. I do not get to my pc very much, in order to check messages. It will be as such for another few weeks, unfortunately. <br \/><br \/>I implore you, in future, to get more specific than \"me into Norway\"."},{"sender":"GERMANY","recipient":"ITALY","time_sent":1897092,"phase":"F1903M","message":"I'm in so-so shape, I think. Hopefully, we will be in a better position to coordinate in spring 1904. I'm playing it safe for now.<br \/>;0)"},{"sender":"ITALY","recipient":"GERMANY","time_sent":1897272,"phase":"F1903M","message":"fair enough. what's your plan with bur? anyway i can help?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":1897607,"phase":"F1903M","message":"Not yet, thank you."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1897852,"phase":"F1903M","message":"sorry to ask again, but i would like to know what burgundy is doing."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":1897884,"phase":"F1903M","message":"Specifics usually follow the generals. I wanted to clarify your intentions given that you have been walking both sides of the street. I will attack from St. Petersburg. Thank you."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1898860,"phase":"F1903M","message":"Could you support me to Albania? I would like to get Trieste empty and that is the best way to do it given that Turkey has already tried to sneak upon me."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1898935,"phase":"F1903M","message":"We get the draw for the next game very soon, and this one is the one that we will get twice out of eight games... What chance I get Austria and you get Italy again?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1898999,"phase":"F1903M","message":"haha, good chance i bet. but to be honest, i quite like italy."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1899022,"phase":"F1903M","message":"much rather have italy than turkey =)"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1899060,"phase":"F1903M","message":"and i don't see any problem with supporting you into alb."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1899172,"phase":"F1903M","message":"Thanks. I appreciate that."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1899330,"phase":"F1903M","message":"but why not use serbia for that? just out of curiosity."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1899366,"phase":"F1903M","message":"in fact, why didn't you use serbia for that last turn?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":1916171,"phase":"F1903M","message":"Volunteer the information for Mar, Spa, Gol first.<br \/><br \/>Ask me about my movements second.<br \/><br \/>That's the order of things, my friend."},{"sender":"GERMANY","recipient":"ITALY","time_sent":1916200,"phase":"F1903M","message":"I am allied with you, so you need fear no attack."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":1916256,"phase":"F1903M","message":"Fair enough. You're welcome."},{"sender":"GERMANY","recipient":"ITALY","time_sent":1916752,"phase":"F1903M","message":"Nor will I support hold or moves from France, obviously."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1921373,"phase":"F1903M","message":"ok, that's all i'm concerned about. as long as you are not attacking me or helping france, we are good. but considering your track record, i wanted to have it crystal clear if you broke an agreement again.<br \/><br \/> i am holding at mar and spa, may move gol to wes, or may not, haven't decided yet. thoughts?"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1923206,"phase":"F1903M","message":"If Serbia moves to Rum (and I'm to take Serbia), the army moving to Rum would have to capture that center."},{"sender":"TURKEY","recipient":"ITALY","time_sent":1923231,"phase":"F1903M","message":"It is convoying."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1923537,"phase":"F1903M","message":"I wouldn't necessarily expect you would end up in Serbia...its possible, but not expected. The Serbian move is mostly to cut support against an insurgence into Rumania. I asked for that mainly to perserve Rumania."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1923704,"phase":"F1903M","message":"Oh, I see."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1923853,"phase":"F1903M","message":"I can do that."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1924028,"phase":"F1903M","message":"ah, i see."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":1924294,"phase":"F1903M","message":"i wanted to let everyone know that i am getting a sitter for thu-sun, so i won't be finalizing over the next few turns. depending on how things go, i hopefully will only need to instruct him to enter the spring moves."}]},{"name":"W1903A","state":{"timestamp":1537459322987912,"zobrist_hash":"6209586366066150720","note":"","name":"W1903A","units":{"AUSTRIA":["A SER","A BUD","A GAL","F ALB"],"ENGLAND":["F NWG","F ENG","F NTH"],"FRANCE":["A BRE","F POR","A GAS","A BUR"],"GERMANY":["A BEL","A SWE","F BAL","F DEN","A RUH","A PIC"],"ITALY":["A MAR","F SPA\/SC","F ION","F WES"],"RUSSIA":["F RUM","A FIN","A UKR","A WAR","F BAR","F NWY"],"TURKEY":["A SMY","A GRE","F AEG","F CON","A BUL"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","PAR","POR"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE"],"ITALY":["NAP","ROM","VEN","TUN","MAR","SPA"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY"],"TURKEY":["ANK","CON","SMY","GRE","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","TRI","VIE","GAL","ALB"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","SKA","ENG","NTH"],"FRANCE":["BRE","POR","PAR","GAS","BUR"],"GERMANY":["BER","KIE","TYR","MUN","BEL","HEL","SWE","HOL","BAL","DEN","RUH","PIC"],"ITALY":["NAP","ROM","VEN","TUN","PIE","MAO","TYS","LYO","MAR","SPA","ION","WES"],"RUSSIA":["MOS","SEV","STP","RUM","BOT","FIN","UKR","WAR","BAR","NWY"],"TURKEY":["ANK","SMY","GRE","AEG","CON","BUL"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":-1,"homes":[]},"GERMANY":{"count":1,"homes":["BER","KIE","MUN"]},"ITALY":{"count":2,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["F POR D"],"GERMANY":["A MUN B"],"ITALY":["F NAP B","A VEN B"],"RUSSIA":[],"TURKEY":[]},"results":{"F POR":[""],"A MUN":[""],"F NAP":[""],"A VEN":[""],"A NWY":["disband"]},"messages":[{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1929557,"phase":"W1903A","message":"Why do you say that?"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1929639,"phase":"W1903A","message":"I told him that he should work with you and I and he said you could 'fuck off'."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1929800,"phase":"W1903A","message":"Yeah...he is probably still quite angry that Germany didn't support hold him in Norway."},{"sender":"ITALY","recipient":"TURKEY","time_sent":1933010,"phase":"W1903A","message":"sorry, when it came down to brass tacs, i had to go with him for now. but i may well be willing to change my track - after all, i also needed to get that fleet out of trieste."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1976098,"phase":"W1903A","message":"the turn before serbia didn't do anything, you would have taken rumania. oh well. <br \/><br \/>do you mind if i build an army at venice that can go west into france?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1976184,"phase":"W1903A","message":"hey there, haven't heard much from you in a while. what's going on? i would still like to work with you, if you are still interested."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1976281,"phase":"W1903A","message":"what are your thoughts for this year? i assume our division so far of france is ok with you? i take por, and you take paris and brest? where are you headed next? if there is further coordination that makes sense for us to do, i'm into that. if not, i'm also ok with each working on our own fronts and just staying out of each others' way."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":1976590,"phase":"W1903A","message":"Sure...but we don't seem to have any common enemies."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1976874,"phase":"W1903A","message":"but we might soon. now that things are settling down on my western front, perhaps it's time to go to work on turkey? but i'll admit to being a bit concerned that if i did that, you and turkey would then roll over me."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1976921,"phase":"W1903A","message":"on another note, i'm curious about your relationship with russia. what do you make of him? do you have peace with him so you can focus on england? or are you going to have to fight them both?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1977838,"phase":"W1903A","message":"Serbia was the broken support...<br \/><br \/>You may build in Venice. You have my trust..."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":1977863,"phase":"W1903A","message":"I am a bit concerned that once Turkey was eliminated you and Austria and probably Germany would finish me off."},{"sender":"GERMANY","recipient":"ITALY","time_sent":1977905,"phase":"W1903A","message":"I don't know what to make of the north after these last orders. I have been playing both sides in an off-hand sort of a way. Nothing treacherous."},{"sender":"GERMANY","recipient":"ITALY","time_sent":1977946,"phase":"W1903A","message":"Nice going with your two builds."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1978091,"phase":"W1903A","message":"thanks. i feel that i'm in a pretty secure position now and have options for what is next. it will be interesting to see what france disbands."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1978100,"phase":"W1903A","message":"Interesting orders. What's the plan for the spring? Will you support Pic-Bre please?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1978115,"phase":"W1903A","message":"alright, fair enough. so back to the austria first plan? and then we can take out turkey?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":1978154,"phase":"W1903A","message":"Nice capture. What's the plan for the spring?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1978255,"phase":"W1903A","message":"excellent, thank you."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":1978648,"phase":"W1903A","message":"I would like to keep working together to capture the English capitals. I will look to capture the Norwegian Sea...and eventually Edinburg. After that, I need to direct my firepower more toward the south."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":1987295,"phase":"W1903A","message":"I had hopes of gaining Brest soon and don't see how I'd gain Norway just yet if Russia holds but I am moving North Sea to Norway regardless."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":2065875,"phase":"W1903A","message":"I am completely OK with going back to the original plan."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":2072383,"phase":"W1903A","message":"Well, let's see how the builds play out. We'll have a clearer picture at that point."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":2072483,"phase":"W1903A","message":"That works for me. <br \/>For the record, I would consider a fleet build at StP south coast to be aggressive. Let's coordinate after the builds."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":2072537,"phase":"W1903A","message":"(after my build, that is)....you will find it to be decidedly non-aggressive."}]},{"name":"S1904M","state":{"timestamp":1537459323000621,"zobrist_hash":"3346511041800957195","note":"","name":"S1904M","units":{"AUSTRIA":["A SER","A BUD","A GAL","F ALB"],"ENGLAND":["F NWG","F ENG","F NTH"],"FRANCE":["A BRE","A GAS","A BUR"],"GERMANY":["A BEL","A SWE","F BAL","F DEN","A RUH","A PIC","A MUN"],"ITALY":["A MAR","F SPA\/SC","F ION","F WES","F NAP","A VEN"],"RUSSIA":["F RUM","A FIN","A UKR","A WAR","F BAR","F NWY"],"TURKEY":["A SMY","A GRE","F AEG","F CON","A BUL"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","PAR","POR"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE"],"ITALY":["NAP","ROM","VEN","TUN","MAR","SPA"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY"],"TURKEY":["ANK","CON","SMY","GRE","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","TRI","VIE","GAL","ALB"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","SKA","ENG","NTH"],"FRANCE":["BRE","POR","PAR","GAS","BUR"],"GERMANY":["BER","KIE","TYR","MUN","BEL","HEL","SWE","HOL","BAL","DEN","RUH","PIC"],"ITALY":["NAP","ROM","VEN","TUN","PIE","MAO","TYS","LYO","MAR","SPA","ION","WES"],"RUSSIA":["MOS","SEV","STP","RUM","BOT","FIN","UKR","WAR","BAR","NWY"],"TURKEY":["ANK","SMY","GRE","AEG","CON","BUL"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A SER S F ALB - GRE","A BUD S A SER","A GAL - VIE","F ALB - GRE"],"ENGLAND":["F NWG S F NTH - NWY","F ENG - MAO","F NTH - NWY"],"FRANCE":["A BRE S A BUR - PAR","A GAS - SPA","A BUR - PAR"],"GERMANY":["A BEL S A RUH - BUR","A SWE H","F BAL S A SWE","A RUH - BUR","A PIC S A RUH - BUR","F DEN S A SWE","A MUN S A RUH - BUR"],"ITALY":["A MAR H","F SPA\/SC S A MAR","F ION H","F WES - MAO","F NAP - APU","A VEN - PIE"],"RUSSIA":["F RUM - BLA","A FIN S F NWY","A UKR - RUM","F BAR - NWG","A WAR - UKR","F NWY S F BAR - NWG"],"TURKEY":["A SMY - CON","A GRE - SER","F AEG - EAS","A BUL S A GRE - SER","F CON - AEG"]},"results":{"A SER":[],"A BUD":[],"A GAL":[],"F ALB":[],"F NWG":["cut"],"F ENG":["bounce"],"F NTH":["bounce"],"A BRE":[],"A GAS":["bounce"],"A BUR":[],"A BEL":[],"A SWE":[],"F BAL":[],"F DEN":[],"A RUH":[],"A PIC":[],"A MUN":[],"A MAR":[],"F SPA\/SC":["cut"],"F ION":[],"F WES":["bounce"],"F NAP":[],"A VEN":[],"F RUM":[],"A FIN":[],"A UKR":[],"A WAR":[],"F BAR":["bounce"],"F NWY":["cut"],"A SMY":[],"A GRE":["bounce","dislodged"],"F AEG":[],"F CON":[],"A BUL":[]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":2114863,"phase":"S1904M","message":"alright, let me see how things are developing."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2114937,"phase":"S1904M","message":"alright, what is going on with your army at munich?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":2151512,"phase":"S1904M","message":"Autumn 1903: Volunteer the information...first.<br \/><br \/>Ask me about my movements second.<br \/><br \/>That's the order of things, my friend."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":2183988,"phase":"S1904M","message":"What's your thoughts on this season's moves?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2210039,"phase":"S1904M","message":"Are we talking to the real djbent here?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":2238610,"phase":"S1904M","message":"I strongly urge you to not continue working with Austria."},{"sender":"GERMANY","recipient":"ITALY","time_sent":2245846,"phase":"S1904M","message":"Did you think I'd leave Mun open with a French army in Bur?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":2246091,"phase":"S1904M","message":"So...I am assuming that you will order Nor-Nrg supported by Bar, and Fin-Nor. Would you like me to support Fin-Nor?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":2246133,"phase":"S1904M","message":"Will you support Den-Nth after?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":2246974,"phase":"S1904M","message":"Mun supports moves to Bur, covers Ber, and my under belly."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":2250345,"phase":"S1904M","message":"No...I will attack from Barents...But I can certainly support DEN-NTH next turn."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":2267150,"phase":"S1904M","message":"not right now... I'm her sitter, FortKnox. She'll be back sunday night."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2272927,"phase":"S1904M","message":"Okay, just wondering. You have my trust on her behalf."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":2279360,"phase":"S1904M","message":"mind if I ask the plan? You are taking greece I assume?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":2279465,"phase":"S1904M","message":"(This is djb's sitter, fortknox): what if I am indifferent towards aus?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2282959,"phase":"S1904M","message":"I would like to, but I will need assistance. I have to attack from Albania as my other units will need to deal with a possible Turkish\/Russia assault.<br \/><br \/>So support for Alb into Greece would be appreciated.<br \/><br \/>dj and I are basically trying to hold up the possibility of a juggernaut. She has had a bit of luck in the West after a West-oriented openning as France did an NMR for the first turn. She could actually go on and win as Italy, which would be marvelous. My hope is to stay with her and get a draw if that is the best we can do."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":2282998,"phase":"S1904M","message":"Would it be better for us to stop fighting? Turkey is between us and you have a rather lively German to deal with."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":2283192,"phase":"S1904M","message":"I agree...I haven't been aggressively pursuing you....so I would like to halt our aggressions. However, you have position on Rumania. What do you suggest?"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":2283441,"phase":"S1904M","message":"I will not hit Rum. It is not a threat to me as it is a fleet anyway, and I am having to deal with the rather aggressive Turk. Indeed, you should move an army to Rum and get on Bul. I will be trying to take Greece, and will help you into Bul if you want, but that would have to be a winter move.<br \/><br \/>If you can get the fleet into the Black Sea and an army into Rum, Bul is yours for sure and Turkey melts.<br \/><br \/>Longer term, I am working with Italy but cannot allow her to get to strong France collapsed). I will be needing to move against Germany as well, so I would like to se an Austrian\/Russian effort that 1) removes the Turk, 2) weakens the Germans and, 3) stops Italy from getting too far ahead. If Turkey is removed soon, we can get a stalemate position between us and ensure that we get a draw at least."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":2283492,"phase":"S1904M","message":"Typo: '... to get too strong (France collapsed)...'"},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":2284019,"phase":"S1904M","message":"Ok...I will make the move against the Turks now. I generally agree with your stated goals. Though I do see #2 and #3 as equally important."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":2284085,"phase":"S1904M","message":"The order was not meant to be a form of ranking, except that we need to remove Turkey quickly and free up units. I look forward to some serious cooperation here."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":2284191,"phase":"S1904M","message":"Incidentally, my open Trieste is by agreement with Italy. She did ask about the army in Venice and wants this to stop the German, with whom she seems to have very bad relations from previous games. I will, given a build, fill Trieste asap and start to move so as to stop her from growing in our direction."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":2284274,"phase":"S1904M","message":"Also, Galicia is moving to Vienna in preparation..."},{"sender":"TURKEY","recipient":"ITALY","time_sent":2284861,"phase":"S1904M","message":"My previous communications would indicate that you're indifferent towards me.<br \/><br \/>I'm willing to work with you. I just need a bit of give on your side. Hell, I'm not even asking for help right now, I'm just asking you to abstain from picking a side."}]},{"name":"F1904M","state":{"timestamp":1537459323014501,"zobrist_hash":"6655690090601338718","note":"","name":"F1904M","units":{"AUSTRIA":["A SER","A BUD","A VIE","F GRE"],"ENGLAND":["F NWG","F ENG","F NTH"],"FRANCE":["A BRE","A GAS","A PAR"],"GERMANY":["A BEL","A SWE","F BAL","F DEN","A PIC","A MUN","A BUR"],"ITALY":["A MAR","F SPA\/SC","F ION","F WES","F APU","A PIE"],"RUSSIA":["A FIN","F BAR","F NWY","F BLA","A RUM","A UKR"],"TURKEY":["A BUL","A CON","F EAS","F AEG"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","PAR","POR"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE"],"ITALY":["NAP","ROM","VEN","TUN","MAR","SPA"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY"],"TURKEY":["ANK","CON","SMY","GRE","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","TRI","GAL","ALB","VIE","GRE"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","SKA","ENG","NTH"],"FRANCE":["BRE","POR","GAS","PAR"],"GERMANY":["BER","KIE","TYR","MUN","BEL","HEL","SWE","HOL","BAL","DEN","RUH","PIC","BUR"],"ITALY":["NAP","ROM","VEN","TUN","MAO","TYS","LYO","MAR","SPA","ION","WES","APU","PIE"],"RUSSIA":["MOS","SEV","STP","BOT","FIN","WAR","BAR","NWY","BLA","RUM","UKR"],"TURKEY":["ANK","SMY","BUL","CON","EAS","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A SER S F GRE","A BUD - TRI","F GRE S A RUM - BUL","A VIE - TYR"],"ENGLAND":["F NWG H","F ENG - BRE","F NTH S F NWG"],"FRANCE":["A BRE S A PAR","A GAS - SPA","A PAR H"],"GERMANY":["A BEL S A BUR","A SWE H","F BAL S A SWE","A PIC - PAR","F DEN - NTH","A MUN - TYR","A BUR S A PIC - PAR"],"ITALY":["A MAR - GAS","F SPA\/SC - POR","F ION H","F WES - MAO","F APU S F ION","A PIE - MAR"],"RUSSIA":["A FIN S F NWY","F BAR - NWG","F NWY S F DEN - NTH","F BLA S A RUM - BUL","A RUM - BUL","A UKR - SEV"],"TURKEY":["A BUL S F ION - GRE","F AEG S F ION - GRE","A CON - ANK","F EAS - SMY"]},"results":{"A SER":[],"A BUD":[],"A VIE":["bounce"],"F GRE":[],"F NWG":[],"F ENG":["bounce"],"F NTH":["cut","dislodged"],"A BRE":["cut"],"A GAS":[],"A PAR":["dislodged"],"A BEL":[],"A SWE":[],"F BAL":[],"F DEN":[],"A PIC":[],"A MUN":["bounce"],"A BUR":[],"A MAR":[],"F SPA\/SC":[],"F ION":[],"F WES":[],"F APU":[],"A PIE":[],"A FIN":[],"F BAR":["bounce"],"F NWY":[],"F BLA":[],"A RUM":[],"A UKR":[],"A BUL":["void","dislodged"],"A CON":[],"F EAS":[],"F AEG":["void"]},"messages":[{"sender":"ITALY","recipient":"GLOBAL","time_sent":2493086,"phase":"F1904M","message":"i am back, btw."},{"sender":"ITALY","recipient":"TURKEY","time_sent":2493120,"phase":"F1904M","message":"can you support me ion-gre?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2493199,"phase":"F1904M","message":"well, that is an option i suppose what is the plan for greece? to support you into bul? that seems fine. i can set up to have three units on trieste next turn, which would be good. or maybe two on greece. have to figure out which."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":2493400,"phase":"F1904M","message":"Would you consider a three way alliance with Russia and me?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":2493425,"phase":"F1904M","message":"Would you consider a three way alliance with Austria and me?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":2493531,"phase":"F1904M","message":"Will you please attack Gas?"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":2493573,"phase":"F1904M","message":"Okay. Will you please attack Brest? Let's get the ball rolling."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2493705,"phase":"F1904M","message":"hey there - that shouldn't be a problem. yes, i will hit gas. what the hell is england doing?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2493738,"phase":"F1904M","message":"so what was up with that? i thought you were joining the feeding frenzy? why are you messing with me?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":2493854,"phase":"F1904M","message":"I am trying to figure that out. He\/she had expressed a desire to capture Brest. I'll let you know if I learn anything."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2493983,"phase":"F1904M","message":"yeah, he told me the same thing. but then he bounced me?! anyway, looks like you and russia reached some sort of amicable but uneasy peace?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":2494285,"phase":"F1904M","message":"So far. I have a Doctorate in Paranoia though...."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2494395,"phase":"F1904M","message":"oh yeah, that seems an important degree to have."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":2494555,"phase":"F1904M","message":"BTW. Russia is attacking Nrg......"},{"sender":"TURKEY","recipient":"ITALY","time_sent":2495200,"phase":"F1904M","message":"If you can somehow confirm that Russia won't be providing support to a move to Bul, then yes."},{"sender":"ITALY","recipient":"TURKEY","time_sent":2495273,"phase":"F1904M","message":"i am waiting to hear what he says. he already asked me to not hit greece."},{"sender":"TURKEY","recipient":"ITALY","time_sent":2495513,"phase":"F1904M","message":"Well, that confirms what I was expecting...try to find out if he's planning on trying for Ank or Con."},{"sender":"ITALY","recipient":"TURKEY","time_sent":2495597,"phase":"F1904M","message":"i am working on it. i am almost tempted to just send ion-alb and apu-ion<br \/><br \/>if you retreat to con with bul, you can disband that fleet at eastern and be in a pretty good defensive position. (bul-con, con-ank, aeg stays put)"},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":2496501,"phase":"F1904M","message":"Yes, certainly as the Italians are the danger. She must not get a good result here..."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":2499764,"phase":"F1904M","message":"What safeguards would I have that you and Austria will not devour me after everyone else is defeated? For a true triple play that balances power equitably, I would think that G\/I\/R is more balanced."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":2513154,"phase":"F1904M","message":"That works for me, as well."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":2515004,"phase":"F1904M","message":"OK...I will see if Italy is interested. I will support Denmark to the North Sea this turn."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":2548668,"phase":"F1904M","message":"How else can I take Brest? Germany is not interested in supporting me so I planned on moving MAO into Brest supported by EC which was going to be occupied soon, why would you need MAO? You can take Portugal uncontested. I only see malicious reasons in that set up."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":2548722,"phase":"F1904M","message":"I will hit Brest to cut its support. Thank you for the warning."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":2583508,"phase":"F1904M","message":"No problem."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":2583542,"phase":"F1904M","message":"...and thank you."},{"sender":"GERMANY","recipient":"ITALY","time_sent":2583940,"phase":"F1904M","message":"I think that we both have way too many units concentrated in the West for our own good. Your shift to Apu was probably a good idea."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2595086,"phase":"F1904M","message":"yes, i was wondering what you were planning with munich?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2599523,"phase":"F1904M","message":"so what is your plan with bul\/con\/ank?i don't really want to see austria get another build, is there a way from turkey to take greece while you pick up one of his SCs?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":2600608,"phase":"F1904M","message":"I think you would have to support a move from Bulgaria to Greece. If you could convince Turkey that this is his best option. No other option give him a chance for no loss of a unit. If Turkey can get Greece back...maybe he can guess right about what I will do....and not lose. <br \/><br \/>I hope he fails :)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2600764,"phase":"F1904M","message":"oh i don't care about turkey not losing a unit - i want him to lose a unit and you to gain one. what i don't want is for austria to gain a unit. so serbia is supporting the hold at greece?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":2600784,"phase":"F1904M","message":"haven't heard from you in a bit, and that has me somewhat concerned."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":2601261,"phase":"F1904M","message":"Yes...that is what I would expect."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2603982,"phase":"F1904M","message":"ok. and you are trying for bul?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":2616821,"phase":"F1904M","message":"any news on your end? what are your thoughts on moves for this turn?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2618540,"phase":"F1904M","message":"Sorry, very busy. The game turns here enable me to leave a game for while.<br \/><br \/>I have managed to secure Russian cooperation. I would also note that I gave your sitter the full trust I have given you: I assumed he would have been under orders to follow your current strategy."},{"sender":"TURKEY","recipient":"ITALY","time_sent":2620271,"phase":"F1904M","message":"I am considering your proposals - regarding retreats."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":2624200,"phase":"F1904M","message":"OK...well I am hearing from Italy that she will work with Turkey to take Greece back. I don't know if that is a ruse or not. My options are to either try for Bulgaria or try to slip my army into ANK. Do you have a preference?"},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":2624289,"phase":"F1904M","message":"I am entering nighttime in my timezone...so I only have a few hours to decide. Before I go to bed I will let you know. I am leaning toward going to Bulgaria."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":2624821,"phase":"F1904M","message":"Okay. If you do, let me know what you need and I will try to accommodate. 'Try' because I must make sure that Bulgaria is not supported into Greece this turn."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2624897,"phase":"F1904M","message":"I am getting disturbing reports of an Italian\/Turkish move on Greece... I hope you can put that one to rest for me. I would so much prefer not to fight after such a fortuitous opening."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":2625043,"phase":"F1904M","message":"So who would support from spreading such a rumor? turkey, i guess. or maybe russia? why on earth would i help turkey take back greece?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":2625128,"phase":"F1904M","message":"alright, well let's figure out a plan. i don't want to get down to the line and not know what i am going to do.<br \/><br \/>it seems someone told austria i am working with you. who have you been talking to? this curtails the options to some degree, but not too much really."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":2625657,"phase":"F1904M","message":"OK....but I got a good sense from Italy that BUL is going to GRE."},{"sender":"TURKEY","recipient":"ITALY","time_sent":2627956,"phase":"F1904M","message":"I haven't told anyone that I was making progress with negotiations.<br \/><br \/>Russia had suggested that I try to get you to work with me, so that the three of us could carve up Austria. I told him that I'd try."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":2629590,"phase":"F1904M","message":"I am sending BUL to RUM supported by BLK S."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2638700,"phase":"F1904M","message":"That is not for me to say, I merely pass on the rumour. I can believe at the least that you do not wish to see Russia grow. But then again, I have to balance Russia against Turkey to have any hope here. If I let Turkey grow, I am dead and you face a naval power in the East. He has already lied to me and tried to sneak into Albania with obvious intentions...<br \/><br \/>I hope we can remain firm here: you have a lot of scope for growth in the West."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":2638757,"phase":"F1904M","message":"Do you mean Rum to Bul? I think so. Thanks for the information."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":2638956,"phase":"F1904M","message":"You have my support here old chap. I am getting disturbing news from spies with regard to the Italians. I hope you can outrun them in the West?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2650023,"phase":"F1904M","message":"btw Where did you go? It was a short trip."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2650218,"phase":"F1904M","message":"REturnng t my leading for you to remain onside with me. The original rationale for us to ally was that we needed to make sure that we stopped someone else from winning. For Italy and Austria to do that would be ideal given the scoring for this Tourney: we would hope to profit as France or Russia etc when our turns came. Right now, the original aim looks very doable. You could even sneak a win, but to strike against me now by crippling my SC count would leave you exosed as you have no hold on the East whatsoever, and a hostile frontier if I turn with germany against you. Clearly, I do not wish to do that. I prefer to keep what we have and making sure that the Russians and Turks remain balanced."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2650254,"phase":"F1904M","message":"Oh dear, desperate typing there... The opening should have read, 'Returning to my pleading...'"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2650303,"phase":"F1904M","message":"I treat women very well too..."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":2660013,"phase":"F1904M","message":"ok that last line was ridiculous. <br \/><br \/>otherwise though, you are right. i think you've taken a rumor a little too seriously, but such is life. you are all set with greece but your build at trieste had better be another army, not a fleet. a fleet will be a pretty clear signal of your intentions to me."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2660171,"phase":"F1904M","message":"Good news. Someone did try hard to split us. My hope is to reduce Turkey to the level of nuisance but not threat. The danger remains Germany."}]},{"name":"F1904R","state":{"timestamp":1537459323017169,"zobrist_hash":"8545379620702753820","note":"","name":"F1904R","units":{"AUSTRIA":["A SER","A VIE","F GRE","A TRI"],"ENGLAND":["F NWG","F ENG","*F NTH"],"FRANCE":["A BRE","A SPA"],"GERMANY":["A BEL","A SWE","F BAL","A MUN","A BUR","F NTH","A PAR"],"ITALY":["F ION","F APU","A GAS","F POR","F MAO","A MAR"],"RUSSIA":["A FIN","F BAR","F NWY","F BLA","A BUL","A SEV"],"TURKEY":["F AEG","A ANK","F SMY","*A BUL"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","PAR","POR"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE"],"ITALY":["NAP","ROM","VEN","TUN","MAR","SPA"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY"],"TURKEY":["ANK","CON","SMY","GRE","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","GAL","ALB","VIE","GRE","TRI"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","SKA","ENG"],"FRANCE":["BRE","SPA"],"GERMANY":["BER","KIE","TYR","MUN","BEL","HEL","SWE","HOL","BAL","DEN","RUH","PIC","BUR","NTH","PAR"],"ITALY":["NAP","ROM","VEN","TUN","TYS","LYO","ION","WES","APU","PIE","GAS","POR","MAO","MAR"],"RUSSIA":["MOS","STP","BOT","FIN","WAR","BAR","NWY","BLA","RUM","UKR","BUL","SEV"],"TURKEY":["CON","EAS","AEG","ANK","SMY"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NTH D"],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":["A BUL R CON"]},"results":{"F NTH":["disband"],"A BUL":[],"A PAR":["disband"]},"messages":[{"sender":"AUSTRIA","recipient":"ITALY","time_sent":2660410,"phase":"F1904R","message":"Please note my moves: I have stopped the Germans and cannot build a fleet!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":2660521,"phase":"F1904R","message":"that is funny. i was thinking to move to tyr. we should have coordinated that, i suppose. well done. and i am glad to see you can't build a fleet."},{"sender":"TURKEY","recipient":"ITALY","time_sent":2663008,"phase":"F1904R","message":"Hrm, seems like Austria isn't going to be playing nice with you anymore.."},{"sender":"ITALY","recipient":"TURKEY","time_sent":2665037,"phase":"F1904R","message":"sorry, i didn't know what the plan was, so i changed my orders. i didn't realize how close we were to the deadline last time we talked.<br \/><br \/>and yes, austria's moves are a bit foreboding. but then so was germany's try for tyrolia.."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":2667937,"phase":"F1904R","message":"I'm sure."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2670735,"phase":"F1904R","message":"alright, turkey failed to make clear his plans in the end, so i went with a more neutral set of moves. i am a bit concerned about having to face off against AH and G, so i will see how things are shaping up this coming turn before dedicating to any change of course. but you should be happy about how things are progressing with turkey?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":2670764,"phase":"F1904R","message":"You're welcome.(re. Tyrolia)"},{"sender":"ITALY","recipient":"GERMANY","time_sent":2670779,"phase":"F1904R","message":"and, that's what i thought you would do. when everything was going so well between us! it's a shame. <br \/><br \/>is there a way we can continue to work together, or are you determined to have war?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":2670804,"phase":"F1904R","message":"haha, well considering both you and austria have said that about tyrolia..."},{"sender":"GERMANY","recipient":"ITALY","time_sent":2670971,"phase":"F1904R","message":"Hey, I owe you one for helping me into Paris. Let me know what I can do."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2671085,"phase":"F1904R","message":"well, glad to see you are wreaking havoc on the english =)<br \/><br \/>let's see how things are developing this turn and where our efforts can best be coordinated."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2671134,"phase":"F1904R","message":"i'm curious though, did you know that AH was moving to tyr, or did you just make that move to mess with us? ;D"},{"sender":"GERMANY","recipient":"ITALY","time_sent":2671270,"phase":"F1904R","message":"I suspected, and I do not want anybody in Tyr except for me(if neccesary)."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2671371,"phase":"F1904R","message":"well, i also don't want anyone in tyr except for me. perhaps another bounce can be arranged there this turn to ensure no funny business happens?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":2671482,"phase":"F1904R","message":"I expect Austria to support his next attempt."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2671740,"phase":"F1904R","message":"well, if that's the case, i blew my opportunity. but we'll see."},{"sender":"GERMANY","recipient":"ITALY","time_sent":2672236,"phase":"F1904R","message":"Don't be negative.<br \/><br \/>We can still kill France, and turn around to deal with Russia and Austria."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2672510,"phase":"F1904R","message":"that sounds good to me."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":2673013,"phase":"F1904R","message":"Germany approached me about a triple alliance. He asked for A\/G\/R. I suggested G\/I\/R. He agreed. I would feel good about a triple alliance with you and Germany. Frankly A\/G would be in a better position to take out Russia than an G\/I alliance. Russia gets part of the Austrian empire with G\/I\/R. He readily agreed and I told him I would speak with you. What do you think?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2673083,"phase":"F1904R","message":"i agree. it sounds good to me. but be veeery careful with him, he will pull random sneaky moves."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2673262,"phase":"F1904R","message":"how would you see moving forward, with such an alliance?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":2673294,"phase":"F1904R","message":"R says you have suggested to him a triple alliance of the three of us. is this true? how do you see it moving forward in the short term?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":2673583,"phase":"F1904R","message":"You should be able to wrap up France here soon. Germany and I will wrap up England. I will continue to build fleets in the North....so that the threat of a combined naval assault by both of us will keep Germany in check. I need one more SC of Turkey and then I can openly go against Austria...so by spring of 1906, I should be able to move against Austria."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2673674,"phase":"F1904R","message":"that sounds good. it looks like AH is getting pushy with me this year. silly me to not move against him, but oh well."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":2676150,"phase":"F1904R","message":"LOL...did you tell England you would support him into Brest? That's just cruel :)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2770005,"phase":"F1904R","message":"do you have any insights into HA's move to tyr? and tri? do you think he is going to try to force tyr this turn?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":2770984,"phase":"F1904R","message":"I haven't heard..."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2772343,"phase":"F1904R","message":"do you mind to do some investigating about what his intentions are toward me?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":2774419,"phase":"F1904R","message":"His intentions are hostile. He and I don't have the kind of relationship where we compare each others moves. Generally, though, I can say he will attack you. Specifically, I cannot say what his movements around Tyrolia will be. This is the retreat phase...then comes build phase. I may know more specifics when the spring actually comes."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2774808,"phase":"F1904R","message":"OK That's what i wanted to confirm. he was all nice and pleasantries, but his actions speak louder than words. looks like a made a mistake not moving on him this last phase. i appreciate any other help you can offer, and once you are able to, helping to attack him"},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":2791462,"phase":"F1904R","message":"Well, is there much I can do to change your course of action?"},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":2791712,"phase":"F1904R","message":"I am always open to negotiation. The past few moves were dictated because of your moves against Albania. I had to work with Russia otherwise you would have out-flanked me."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":2791858,"phase":"F1904R","message":"Let me know what you are thinking. I will reply later - I have to work now (different time zone)."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":2791953,"phase":"F1904R","message":"Well, I'd LOVE to see you move to Bul."}]},{"name":"W1904A","state":{"timestamp":1537459323019181,"zobrist_hash":"547225799255425965","note":"","name":"W1904A","units":{"AUSTRIA":["A SER","A VIE","F GRE","A TRI"],"ENGLAND":["F NWG","F ENG"],"FRANCE":["A BRE","A SPA"],"GERMANY":["A BEL","A SWE","F BAL","A MUN","A BUR","F NTH","A PAR"],"ITALY":["F ION","F APU","A GAS","F POR","F MAO","A MAR"],"RUSSIA":["A FIN","F BAR","F NWY","F BLA","A BUL","A SEV"],"TURKEY":["F AEG","A ANK","F SMY","A CON"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER","GRE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE","PAR"],"ITALY":["NAP","ROM","VEN","TUN","MAR","POR"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","GAL","ALB","VIE","GRE","TRI"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","SKA","ENG"],"FRANCE":["BRE","SPA"],"GERMANY":["BER","KIE","TYR","MUN","BEL","HEL","SWE","HOL","BAL","DEN","RUH","PIC","BUR","NTH","PAR"],"ITALY":["NAP","ROM","VEN","TUN","TYS","LYO","ION","WES","APU","PIE","GAS","POR","MAO","MAR"],"RUSSIA":["MOS","STP","BOT","FIN","WAR","BAR","NWY","BLA","RUM","UKR","BUL","SEV"],"TURKEY":["EAS","AEG","ANK","SMY","CON"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":1,"homes":["BUD"]},"ENGLAND":{"count":1,"homes":["EDI","LON","LVP"]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":1,"homes":["BER","KIE"]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":1,"homes":["MOS","STP","WAR"]},"TURKEY":{"count":-1,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD B"],"ENGLAND":["A LVP B"],"FRANCE":[],"GERMANY":["F KIE B"],"ITALY":[],"RUSSIA":["A MOS B"],"TURKEY":["F AEG D"]},"results":{"A BUD":[""],"A LVP":[""],"F KIE":[""],"A MOS":[""],"F AEG":[""]},"messages":[{"sender":"ITALY","recipient":"ENGLAND","time_sent":3019988,"phase":"W1904A","message":"i was going to offer you support into brest this turn, but then i realized you might need to use EC to cover London. what are you planning for this turn? i am not interested in fighting germany just yet, but i'm happy to help you out."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3020037,"phase":"W1904A","message":"i am a bit concerned that i haven't heard from you in a while. and people are telling me that you have ill-intentions toward me. will our alliance truly be so short lived?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":3020950,"phase":"W1904A","message":"Nothing sinister. I have been quiet because this is a build phase in which my only option is an army or to not build at all... I am all for the alliance and will see it through. You should have no concerns there."}]},{"name":"S1905M","state":{"timestamp":1537459323032757,"zobrist_hash":"3286086195849512937","note":"","name":"S1905M","units":{"AUSTRIA":["A SER","A VIE","F GRE","A TRI","A BUD"],"ENGLAND":["F NWG","F ENG","A LVP"],"FRANCE":["A BRE","A SPA"],"GERMANY":["A BEL","A SWE","F BAL","A MUN","A BUR","F NTH","A PAR","F KIE"],"ITALY":["F ION","F APU","A GAS","F POR","F MAO","A MAR"],"RUSSIA":["A FIN","F BAR","F NWY","F BLA","A BUL","A SEV","A MOS"],"TURKEY":["A ANK","F SMY","A CON"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER","GRE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE","PAR"],"ITALY":["NAP","ROM","VEN","TUN","MAR","POR"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","GAL","ALB","VIE","GRE","TRI"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","SKA","ENG"],"FRANCE":["BRE","SPA"],"GERMANY":["BER","KIE","TYR","MUN","BEL","HEL","SWE","HOL","BAL","DEN","RUH","PIC","BUR","NTH","PAR"],"ITALY":["NAP","ROM","VEN","TUN","TYS","LYO","ION","WES","APU","PIE","GAS","POR","MAO","MAR"],"RUSSIA":["MOS","STP","BOT","FIN","WAR","BAR","NWY","BLA","RUM","UKR","BUL","SEV"],"TURKEY":["EAS","AEG","ANK","SMY","CON"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A SER H","F GRE H","A VIE - BOH","A TRI - TYR","A BUD - VIE"],"ENGLAND":["F NWG - EDI","F ENG - LON","A LVP - YOR"],"FRANCE":["A BRE S A SPA - GAS","A SPA - GAS"],"GERMANY":["A BEL - YOR VIA","A SWE H","F BAL S A SWE","A MUN - BUR","A BUR - BEL","F NTH C A BEL - YOR","A PAR S A BRE","F KIE - DEN"],"ITALY":["F ION H","F APU - VEN","A MAR - PIE","A GAS - MAR","F MAO S F POR - SPA","F POR - SPA\/SC"],"RUSSIA":["A FIN S F NWY","F BAR - NWG","F NWY S F BAR - NWG","F BLA - ANK","A BUL - CON","A SEV - RUM","A MOS - SEV"],"TURKEY":["A ANK S A CON","F SMY - AEG","A CON H"]},"results":{"A SER":[],"A VIE":[],"F GRE":[],"A TRI":[],"A BUD":[],"F NWG":[],"F ENG":[],"A LVP":["bounce"],"A BRE":[],"A SPA":[],"A BEL":["bounce"],"A SWE":[],"F BAL":[],"A MUN":["bounce"],"A BUR":["bounce"],"F NTH":[],"A PAR":[],"F KIE":[],"F ION":[],"F APU":[],"A GAS":[],"F POR":[],"F MAO":[],"A MAR":[],"A FIN":[],"F BAR":[],"F NWY":[],"F BLA":["bounce"],"A BUL":["bounce"],"A SEV":[],"A MOS":[],"A ANK":["cut"],"F SMY":[],"A CON":[]},"messages":[{"sender":"ITALY","recipient":"GERMANY","time_sent":3043749,"phase":"S1905M","message":"very interesting build on England's part. makes life easier for you perhaps."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3043954,"phase":"S1905M","message":"I know you had said you would wait until 1906 to attack Austria, but I wonder if you'd be willing to try this turn? i think that turkey is not a threat at all, and you and i can keep him contained while we take a bite out of austria. then i can help you finish turkey off. i would envision the turkish SCs to be yours, and the majority of the austrian ones to be mine. what do you think?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":3044029,"phase":"S1905M","message":"well chrisp, i'm really sorry that in another game we have ended up as enemies. seems like you've been less active on the site in general lately, so i hope all is well, and though you still might make it far in this game, that we get to play another sometime and not be enemies from the start =)"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3047417,"phase":"S1905M","message":"I am in zero position to hit Austria that I can see....did you have a specific move?"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":3051089,"phase":"S1905M","message":"Please do not worry about my build: I had no choice. I will not try to grab Rum."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":3051371,"phase":"S1905M","message":"I would love Brest in hopes that Germany assumes I'll defend London."},{"sender":"FRANCE","recipient":"ITALY","time_sent":3052317,"phase":"S1905M","message":"Haha, no worries. But yes, I've been less active on the site because my interest has been waning and real life just keeps getting bigger. =)<br \/><br \/>I'm going to try and keep only one active game at a time soon, and there's a small chance that I'll ask Kestas to name a new moderator to make up for my lack of activity. *cough* I'm looking at you *cough*"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3099144,"phase":"S1905M","message":"No, you're right, you don't have a good leg up on him. is there any way for you to get into a better position against him?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":3099174,"phase":"S1905M","message":"i don't suppose i could get you to move smy-aeg? i would offer support."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3099209,"phase":"S1905M","message":"what's your plan for this turn? a bit of time left to decide i suppose. but i think it's time we started coordinating against austria, no?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":3099259,"phase":"S1905M","message":"alright, well the problem is that i need to move units back east to protect against the incoming stab from austria. so, i can't support you this turn. but in autumn. would that work?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":3129078,"phase":"S1905M","message":"I've got time, if not, oh well. lol ^_^"},{"sender":"TURKEY","recipient":"ITALY","time_sent":3140939,"phase":"S1905M","message":"What benefit would that be to us?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":3141013,"phase":"S1905M","message":"it would put two units on greece. and block AH from getting into Aeg. which i am sure will be his next move."},{"sender":"ITALY","recipient":"TURKEY","time_sent":3141077,"phase":"S1905M","message":"but if you prefer not to, i am fine with that. alternatively, we could bounce there. that would at least accomplish one goal."},{"sender":"TURKEY","recipient":"ITALY","time_sent":3181714,"phase":"S1905M","message":"I can move there."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":3181731,"phase":"S1905M","message":"Any thoughts on my suggestion that you take Bul?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":3184625,"phase":"S1905M","message":"so do you want support, or a bounce?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3191970,"phase":"S1905M","message":"And yet you have not shared anything about your plans for this turn.<br \/><br \/>I am going to cover myself against a potential stab from you. I am going to take spain, and that's pretty much my plans this time around. you?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":3192033,"phase":"S1905M","message":"hm, i haven't heard from you at all. i'm not too worried b\/c i think we are workingw ell together and only have more to benefit from that, but i would like to check in and hear what you are planning. i hope i can count on your continued assistance if austria does attack me?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":3192056,"phase":"S1905M","message":"ok, thanks. i can definitely support you in in autumn."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3192086,"phase":"S1905M","message":"not very talkative this turn. what are you planning to do? i'm going to cover against austria, and hope for the best."},{"sender":"GERMANY","recipient":"ITALY","time_sent":3192353,"phase":"S1905M","message":"Absolutely. I am, of course, fighting England and France."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3192463,"phase":"S1905M","message":"right. and doing a fine job of it, i must say. is munich still available to help against austria, or do you have other plans for that army?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":3192504,"phase":"S1905M","message":"Sorry - very busy these past two days. Not sure here. I have good relations with the Russians for now and cannot afford to spoil that. I cannot get at Turkey either, so I think a push north would be in order. I think you will like that."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":3192567,"phase":"S1905M","message":"I am looking at it but unless I get a support from you the move could bounce and I just earn the wrath of the Russians. What would be your hope for after I took Bul?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3192574,"phase":"S1905M","message":"i will like anything that is not an attack on me. and i understand about being busy, it's been the same for me.<br \/><br \/>looks like a push north will face very little resistance."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":3192707,"phase":"S1905M","message":"Yes, now seems to be a good time to do it. I will attack Tyrolia and Boh, I will get one of them, and have the leverage for the other next turn. Venice is quite safe."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3192755,"phase":"S1905M","message":"I hope to get lucky against Turkey and increase my position against Austria."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":3192756,"phase":"S1905M","message":"Any thoughts on what to do now? We have a stable border but the Turk has gone into hedgehog mode."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3192790,"phase":"S1905M","message":"it would feel safer if tri were not occupied. is that part of the plan?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3192817,"phase":"S1905M","message":"great, that would make me happy. if i learn anything of turkey's moves, i'll let you know."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":3192846,"phase":"S1905M","message":"Yes, but Trieste is likely to bounce at Tyr this move, but can be pulled into Tyr in the next one."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":3193256,"phase":"S1905M","message":"I am hoping to catch Turkey making a mistake. I figure I have a 50\/50 shot against him. I understand about the build. It is no problem. Hopefully Italy can be contained until I could assist you in breaking the Ionian."},{"sender":"GERMANY","recipient":"ITALY","time_sent":3193326,"phase":"S1905M","message":"Unfortunately, Mun is otherwise occupied this term. Aus will support his order to Tyr anyway. Mun would be useless there, for now."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":3193368,"phase":"S1905M","message":"Germany blocked you in Munich. Do you think he will be a problem this turn?"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":3193419,"phase":"S1905M","message":"Italy is very nervous about me stabbing but I am playing a very honourable game with her and will not show any false moves until it is possible to do so and profit."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3193510,"phase":"S1905M","message":"alright. i'll deal."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3193521,"phase":"S1905M","message":"sounds good to me."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3193567,"phase":"S1905M","message":"but remember you owe me one from the help with paris ;D"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":3193799,"phase":"S1905M","message":"Okay, I will go ahead then. Be ready for German anger."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3193815,"phase":"S1905M","message":"oh, i am ready haha."},{"sender":"GERMANY","recipient":"ITALY","time_sent":3193837,"phase":"S1905M","message":"I remember."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3194191,"phase":"S1905M","message":"would you be amenable to me getting either brest or an english SC in the near future to help me fight AH? i know that's your sphere of influence, but i think we could arrange something that keeps us both well balanced and out of each other's hair. i'm thinking lvp, i suppose."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":3194317,"phase":"S1905M","message":"Well, I would support you into Bul.<br \/><br \/>Once you've taken Bul, Rum would be the next potential target.<br \/><br \/>I would be able to potentially move some units to pressure Sev."},{"sender":"TURKEY","recipient":"ITALY","time_sent":3194329,"phase":"S1905M","message":"Support is good."}]},{"name":"F1905M","state":{"timestamp":1537459323047278,"zobrist_hash":"1260734082172640663","note":"","name":"F1905M","units":{"AUSTRIA":["A SER","F GRE","A BOH","A TYR","A VIE"],"ENGLAND":["A LVP","F EDI","F LON"],"FRANCE":["A BRE","A GAS"],"GERMANY":["A BEL","A SWE","F BAL","A MUN","A BUR","F NTH","A PAR","F DEN"],"ITALY":["F ION","F MAO","F VEN","A MAR","F SPA\/SC","A PIE"],"RUSSIA":["A FIN","F NWY","F BLA","A BUL","F NWG","A RUM","A SEV"],"TURKEY":["A ANK","A CON","F AEG"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER","GRE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE","PAR"],"ITALY":["NAP","ROM","VEN","TUN","MAR","POR"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","SER","GAL","ALB","GRE","TRI","BOH","TYR","VIE"],"ENGLAND":["LVP","YOR","SKA","ENG","EDI","LON"],"FRANCE":["BRE","GAS"],"GERMANY":["BER","KIE","MUN","BEL","HEL","SWE","HOL","BAL","RUH","PIC","BUR","NTH","PAR","DEN"],"ITALY":["NAP","ROM","TUN","TYS","LYO","ION","WES","APU","POR","MAO","VEN","MAR","SPA","PIE"],"RUSSIA":["MOS","STP","BOT","FIN","WAR","BAR","NWY","BLA","UKR","BUL","NWG","RUM","SEV"],"TURKEY":["EAS","ANK","SMY","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A SER S F GRE","F GRE S A BUL","A BOH S A TYR - MUN","A TYR - MUN","A VIE - BUD"],"ENGLAND":["A LVP - YOR","F EDI S F LON - NTH","F LON - NTH"],"FRANCE":["A BRE S A GAS - PAR","A GAS - PAR"],"GERMANY":["A BEL - EDI VIA","A SWE - LVN VIA","F BAL C A SWE - LVN","A MUN S A PIE - TYR","A BUR S A MUN","F NTH C A BEL - EDI","A PAR S A BUR","F DEN - SWE"],"ITALY":["F ION - GRE","F MAO - ENG","F SPA\/SC - MAO","F VEN - TRI","A PIE - TYR","A MAR - SPA"],"RUSSIA":["A FIN S F NWY","F NWY S F NWG","F BLA S A BUL","A BUL S A RUM - SER","A RUM - SER","A SEV - RUM","F NWG S F NTH - EDI"],"TURKEY":["A ANK S A CON","A CON S A BUL","F AEG S F ION - GRE"]},"results":{"A SER":["cut","dislodged"],"F GRE":["cut","dislodged"],"A BOH":[],"A TYR":["bounce","dislodged"],"A VIE":[],"A LVP":[],"F EDI":[],"F LON":[],"A BRE":[],"A GAS":[],"A BEL":["no convoy"],"A SWE":[],"F BAL":[],"A MUN":[],"A BUR":[],"F NTH":["dislodged"],"A PAR":["cut","dislodged"],"F DEN":[],"F ION":[],"F MAO":[],"F VEN":[],"A MAR":[],"F SPA\/SC":[],"A PIE":[],"A FIN":[],"F NWY":[],"F BLA":[],"A BUL":[],"F NWG":["void"],"A RUM":[],"A SEV":[],"A ANK":[],"A CON":[],"F AEG":[]},"messages":[{"sender":"TURKEY","recipient":"RUSSIA","time_sent":3212903,"phase":"F1905M","message":"Do we really need to be enemies? You're in a great position to take a big chunk of Austria out, and I can help you do that."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3213733,"phase":"F1905M","message":"curious move with paris supporting the hold at brest. <br \/><br \/>not nearly as curious as austria's all-out attack on you. damn. forutnately, it leaves him wide open to a stab from russia."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3213802,"phase":"F1905M","message":"very interesting moves this turn. what are you thinking next? now it's my turn to be out of position, but i think i can make a recovery."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3213881,"phase":"F1905M","message":"well, i am glad to see you kept good on your word - i really doubted that you would! sorry about that. but i can happy to move back into position now to pressure turkey. that will be great. maybe i can even take advantage of france's position to support him against germany."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":3221482,"phase":"F1905M","message":"I got he replay too late and had already decided to pressure Germany. However, I am still talking and would consider an operation."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":3221530,"phase":"F1905M","message":"Can you tap Burgundy? It gives me Munich."},{"sender":"GERMANY","recipient":"ITALY","time_sent":3223772,"phase":"F1905M","message":"Will you please support Mun-Tyr?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":3224033,"phase":"F1905M","message":"Will you please support Bel-Edi?"},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":3227289,"phase":"F1905M","message":"I will; however, I did express interest in Edinburg in previous conversations. Are you agreeable to my control of this center within .... say the next 2 years?<br \/><br \/>Also Austria appears at your border. Was that anticipated or expected?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3227492,"phase":"F1905M","message":"You have thus far remained neutral with Austria and Turkey. Are you going to be assisting Austria in his attack on Munich?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":3227530,"phase":"F1905M","message":"OK....I am considering your offer and our positions. Give me about 24 hours to consider it."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3233982,"phase":"F1905M","message":"well, what do you think i should do?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":3275308,"phase":"F1905M","message":"Thank you. I will swap Edi for Lvp later on. I did not think that Austria would be so aggressive."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3276908,"phase":"F1905M","message":"in exchange for your support into brest, yes."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3276961,"phase":"F1905M","message":"one option would be for me to get support from turkey into greece, and you to support yourself into serbia. what do you think of that?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":3277020,"phase":"F1905M","message":"nicely done. now i can't support you though =("},{"sender":"ITALY","recipient":"TURKEY","time_sent":3277054,"phase":"F1905M","message":"would you be willing to support me into greece this turn?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3277072,"phase":"F1905M","message":"and gives me a new enemy."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3277108,"phase":"F1905M","message":"but i am willing to do it. let me think on it."},{"sender":"TURKEY","recipient":"ITALY","time_sent":3277465,"phase":"F1905M","message":"I'd prefer the other way around. But I'm not really in a strong position to negotiate, am I?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3277530,"phase":"F1905M","message":"OK....I would be cool with that if we can get Turkey to agree."},{"sender":"ITALY","recipient":"TURKEY","time_sent":3277591,"phase":"F1905M","message":"nope, you're not. if you do support me, russia will hit austria as well. taking some of the heat off of you."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3277658,"phase":"F1905M","message":"great."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3277682,"phase":"F1905M","message":"it sounds like he will, but i don't have a confirmation from him yet."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":3278761,"phase":"F1905M","message":"true but better than a German in England"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":3279549,"phase":"F1905M","message":"yes, true. i might move to channel to see if i can help you out."},{"sender":"FRANCE","recipient":"ITALY","time_sent":3291380,"phase":"F1905M","message":"Think you can hit Bur? =)"},{"sender":"ITALY","recipient":"FRANCE","time_sent":3297402,"phase":"F1905M","message":"you're not the first to ask. what do i get out of it, aside from a very angry german?"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":3312975,"phase":"F1905M","message":"Any word on my proposal?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":3312995,"phase":"F1905M","message":"Ion - Gre then?"},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":3313014,"phase":"F1905M","message":"Russia and Italy are working on a deal to eliminate you."},{"sender":"FRANCE","recipient":"ITALY","time_sent":3313183,"phase":"F1905M","message":"Well, clearly if I'm not the first to ask then you have something to get out of it from someone else. You'll earn my gratitude, however much that is worth to you. =P<br \/><br \/>Also, I think it's quite clear that the Germany-Russian alliance has to be combatted, don't you?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":3315508,"phase":"F1905M","message":"Yes....I will attack Serbia. Italy is asking for support to Greece....I believe. If you could do that....then I think I can get you Greece next year....Serbia has nothing to support hold his position."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":3361052,"phase":"F1905M","message":"I am supporting Bul this turn just in case Turkey gets nasty there."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":3361128,"phase":"F1905M","message":"I am hitting Munich from Tyrolia. Simple. I leave myself entirely in your hands. Meanwhile, I am trying to set a proper defence in case the Russians get greedy. A build from Munich would go a long way to helping me do that."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3361702,"phase":"F1905M","message":"that sounds good."},{"sender":"GERMANY","recipient":"ITALY","time_sent":3361817,"phase":"F1905M","message":"Actually, bad idea for me. Will you please just attack Tyr instead?"},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":3361859,"phase":"F1905M","message":"Since when is aggression towards ME(of all people) a healthy idea?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":3361893,"phase":"F1905M","message":"yeah, i wasn't sure what you were thinking there. what do you want to do about france?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":3361930,"phase":"F1905M","message":"and i would still like to hear why you supported france's hold at brest."},{"sender":"GERMANY","recipient":"ITALY","time_sent":3361938,"phase":"F1905M","message":"Hold fast, and wait for the disbands."},{"sender":"GERMANY","recipient":"ITALY","time_sent":3362166,"phase":"F1905M","message":"Easy answer. Brest is mine in any 17-17 split between us. You have Iberia, and Mar. You also control Mid(which I am grateful for).<br \/><br \/>You can have Vie, Tri, Bud, Ser,Gre, Rum, Bul, Con, Ank, and Smy."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3362441,"phase":"F1905M","message":"yes, but in the short run it makes more sense for me to take it rather than keep france alive, no?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3362531,"phase":"F1905M","message":"on a different note, what do you want to do about turkey? just going to chill in greece and ignore his fleet at aeg, or did you have other plans?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":3363266,"phase":"F1905M","message":"Well, order the attack. I'll use the time between now and adjudications to decide whether or not you and Austria are in cahoots."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3363908,"phase":"F1905M","message":"why would i be in cahoots with him? i thought i was in cahoots with you and russia?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":3364408,"phase":"F1905M","message":"So did I. Mostly with me though. I am supporting Pie-Tyr btw."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3364569,"phase":"F1905M","message":"well, i think i might move pie-ven and ven-tri. i'm trying to learn austria's move first. if it looks like he's going to cover tri, pie-tyr might well work."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3364674,"phase":"F1905M","message":"i'm at a turning point. i can either be part of an alliance with G and R, or fight them with AH. i am not inclined to share my plans, but i will point out that germany can't cover both paris and munich, and he has hostile forces bordering both."},{"sender":"ITALY","recipient":"TURKEY","time_sent":3371461,"phase":"F1905M","message":"that would be excellent. it should buy you another turn from russia, and in a few i might be able to help you out."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3371507,"phase":"F1905M","message":"turkey has agreed. so will you hit serbia? <br \/><br \/>what's your plan in the north? i'm trying to get a sense of the balance of power."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3371551,"phase":"F1905M","message":"sorry, one more question. how are things between you and germany? he can be a real jerk, so i know it's hard to be in an alliance with him. but are all three of us still on the same team?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":3373113,"phase":"F1905M","message":"Oh I noticed that. But he can not cover either with your help. =)"},{"sender":"ITALY","recipient":"FRANCE","time_sent":3373628,"phase":"F1905M","message":"that is true. i'm not sure i want to do that though. i don't mind to see an ally weakened, but losing one outright? that's a larger decision.<br \/><br \/>if i were to do it, would you consider trading me brest for paris?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":3373776,"phase":"F1905M","message":"No, I'm not THAT interested in helping Austria out hahaha. That's fine if you don't want to hit Bur... but can I ask that you do not hit Brest? <br \/><br \/>I have no doubt that you will probably mop me up soon after, but I don't see why we can't be friends until then. =)"},{"sender":"ITALY","recipient":"FRANCE","time_sent":3374280,"phase":"F1905M","message":"helping austria?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3375114,"phase":"F1905M","message":"Yes...I will hit Serbia. Germany will try for Edinburg...with my support...which will probably fail. Germany is taking the lead at this point...at least until I can build forces in the West to exert some force on him.<br \/><br \/>He and I actually talk very little. Maybe every other turn."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3375234,"phase":"F1905M","message":"well, i have been asked to hit burgundy by both france and austria. i've considered it. but if i am stabbing austria, i probably shouldn't make two new enemies in one turn. but i agree that germany is getting into a dominant position. how do you think that should play into our plans? should we wait a bit longer to hit austria? seems like it's the opportune moment for a stab on him though."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3375719,"phase":"F1905M","message":"Yeah...Austria is definitely out of position. Germany was the one who wanted Austria in the 3 way alliance...I was the one who wanted you. So, I have to feel that Germany must have his pride somewhat stung to have suggested Austria. Germany is also somewhat immature...I don't think he bounces back from a stab...like say how Turkey is playing right now. I guess you just need to take that into account re: your decision."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3375968,"phase":"F1905M","message":"that is very true, i don't think he would bounce back."},{"sender":"FRANCE","recipient":"ITALY","time_sent":3376039,"phase":"F1905M","message":"Well, if I were Germany, I'd defend Munich over Paris so you hitting Burgundy is more likely to help Austria than me, so I would not be willing to give you Brest to hit Burgundy."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3376111,"phase":"F1905M","message":"and i'm sure he wanted austria in the three way b\/c that would benefit him the most - leaves him easy pickings in the west, while you and AH would be bottled up on the east side, and AH would have to contend with me. <br \/><br \/>would you still want to stab AH if i decided not to do so this turn? it seems like your best bet for getting a build, and i am sensitive to making sure you get to keep growing."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3376189,"phase":"F1905M","message":"true, and true. alright, it's agreed that i won't hit paris. so your new request is that i not hit brest. interesting idea... i might be willing to do just that. especially sine G has refused to support me in. that's what he gets...i'll get back to you in a little bit with my decisions."},{"sender":"FRANCE","recipient":"ITALY","time_sent":3376643,"phase":"F1905M","message":"If you don't hit Brest, the accounting will basically reflect that you've taken one of Germany's SC's without ever attacking him. =P"},{"sender":"ITALY","recipient":"FRANCE","time_sent":3376696,"phase":"F1905M","message":"yup, that's what i'm thinking..."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3376785,"phase":"F1905M","message":"ok, i've thought about it a bit more, and i think that i am down for still hitting austria, no change of plans. germany is going to lose either mun or par, so that's fine =)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3376820,"phase":"F1905M","message":"and i won't have anything to do with it, best of all."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3381604,"phase":"F1905M","message":"ok, ill try for tyr, it can't hurt. i don't think he'll try for ven, and maybe i'll take tyr. we'll see."}]},{"name":"F1905R","state":{"timestamp":1537459323050378,"zobrist_hash":"348689831937711725","note":"","name":"F1905R","units":{"AUSTRIA":["A BOH","A BUD","*A SER","*F GRE","*A TYR"],"ENGLAND":["F EDI","A YOR","F NTH"],"FRANCE":["A BRE","A PAR"],"GERMANY":["A BEL","F BAL","A MUN","A BUR","A LVN","F SWE","*F NTH","*A PAR"],"ITALY":["F GRE","F ENG","F TRI","A SPA","F MAO","A TYR"],"RUSSIA":["A FIN","F NWY","F BLA","A BUL","F NWG","A SER","A RUM"],"TURKEY":["A ANK","A CON","F AEG"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","SER","GRE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE","PAR"],"ITALY":["NAP","ROM","VEN","TUN","MAR","POR"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["GAL","ALB","BOH","VIE","BUD"],"ENGLAND":["LVP","SKA","EDI","LON","YOR","NTH"],"FRANCE":["BRE","GAS","PAR"],"GERMANY":["BER","KIE","MUN","BEL","HEL","HOL","BAL","RUH","PIC","BUR","DEN","LVN","SWE"],"ITALY":["NAP","ROM","TUN","TYS","LYO","ION","WES","APU","POR","VEN","MAR","PIE","GRE","ENG","TRI","SPA","MAO","TYR"],"RUSSIA":["MOS","STP","BOT","FIN","WAR","BAR","NWY","BLA","UKR","BUL","NWG","SEV","SER","RUM"],"TURKEY":["EAS","ANK","SMY","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A SER R ALB","A TYR R VEN","F GRE D"],"ENGLAND":[],"FRANCE":[],"GERMANY":["A PAR R PIC","F NTH R HOL"],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A SER":[],"F GRE":["disband"],"A TYR":[],"F NTH":[],"A PAR":[]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":3391832,"phase":"F1905R","message":"damn. that sucks. i did not anticipate that stab by germany at all. but nicely done with austria, and i think you and i can take on germany and austria together."},{"sender":"ITALY","recipient":"TURKEY","time_sent":3391863,"phase":"F1905R","message":"thank you!"},{"sender":"ITALY","recipient":"GERMANY","time_sent":3392066,"phase":"F1905R","message":"thanks for the support into tyr, sorry to see what happened in north sea and paris. and what are you thinking with the move to lvn? i don't see the sense there. but you should be happy that austria just got destroyed. ;)"},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":3393034,"phase":"F1905R","message":"Damn...this whole turn was one cruel move."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3393905,"phase":"F1905R","message":"How could you have known? It was a pretty piss poor stab. I expected better."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":3394771,"phase":"F1905R","message":"Good question. All I can do now is to fight a rearguard action."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3402464,"phase":"F1905R","message":"yeah, a bit ridiculous. typical of him, though, really."}]},{"name":"W1905A","state":{"timestamp":1537459323052249,"zobrist_hash":"7361895589751215719","note":"","name":"W1905A","units":{"AUSTRIA":["A BOH","A BUD","A ALB","A VEN"],"ENGLAND":["F EDI","A YOR","F NTH"],"FRANCE":["A BRE","A PAR"],"GERMANY":["A BEL","F BAL","A MUN","A BUR","A LVN","F SWE","A PIC","F HOL"],"ITALY":["F GRE","F ENG","F TRI","A SPA","F MAO","A TYR"],"RUSSIA":["A FIN","F NWY","F BLA","A BUL","F NWG","A SER","A RUM"],"TURKEY":["A ANK","A CON","F AEG"]},"centers":{"AUSTRIA":["BUD","VIE","VEN"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","PAR"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE"],"ITALY":["NAP","ROM","TUN","MAR","POR","TRI","GRE","SPA"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["GAL","BOH","VIE","BUD","ALB","VEN"],"ENGLAND":["LVP","SKA","EDI","LON","YOR","NTH"],"FRANCE":["BRE","GAS","PAR"],"GERMANY":["BER","KIE","MUN","BEL","HEL","BAL","RUH","BUR","DEN","LVN","SWE","PIC","HOL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","ION","WES","APU","POR","MAR","PIE","GRE","ENG","TRI","SPA","MAO","TYR"],"RUSSIA":["MOS","STP","BOT","FIN","WAR","BAR","NWY","BLA","UKR","BUL","NWG","SEV","SER","RUM"],"TURKEY":["EAS","ANK","SMY","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":-1,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":-1,"homes":[]},"ITALY":{"count":2,"homes":["NAP","ROM"]},"RUSSIA":{"count":1,"homes":["MOS","SEV","STP","WAR"]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A ALB D"],"ENGLAND":[],"FRANCE":[],"GERMANY":["F BAL D"],"ITALY":["F NAP B","A ROM B"],"RUSSIA":["A MOS B"],"TURKEY":[]},"results":{"A ALB":[""],"F BAL":[""],"F NAP":[""],"A ROM":[""],"A MOS":[""]},"messages":[{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":3458086,"phase":"W1905A","message":"Do you repent your presumptuous flirtation with my beloved Munich?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":3458216,"phase":"W1905A","message":"As for Russia, he\/she promised support that was not given. Livonia is my demonstrative way of saying \"tsk tsk tsk\"."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":3458327,"phase":"W1905A","message":"OK. Now let's talk. Thank you for the lack of support into Edi, and you're welcome for my incursion into Liv."},{"sender":"GERMANY","recipient":"FRANCE","time_sent":3458487,"phase":"W1905A","message":"Are you ready to communicate?<br \/><br \/>Should I bother? <br \/><br \/>In one year, you've yet to honour ONE agreement."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3459761,"phase":"W1905A","message":"ah and i messed up, i shouldn't have forced Tyr. oops! lol"},{"sender":"ITALY","recipient":"GLOBAL","time_sent":3459791,"phase":"W1905A","message":"damn i am stupid. i knew there was a reason to not attack tyrolia...."},{"sender":"TURKEY","recipient":"ITALY","time_sent":3460247,"phase":"W1905A","message":"You should be able to get Ven back reasonably easily."},{"sender":"ITALY","recipient":"TURKEY","time_sent":3460512,"phase":"W1905A","message":"i hope so. that was really stupid of me. the whole turn i was resisting the move to tyr, i can't believe i put it in in the end!!"},{"sender":"FRANCE","recipient":"GERMANY","time_sent":3465367,"phase":"W1905A","message":"It doesn't hurt to try."},{"sender":"TURKEY","recipient":"ITALY","time_sent":3469759,"phase":"W1905A","message":"If you build armies in Rom and Nap, you've got it back in the fall, easy."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":3480374,"phase":"W1905A","message":"What do you suggest? Obviously, I am looking for Livonia to disband."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":3484872,"phase":"W1905A","message":"Absolutely. Indeed, the women are too big for my tastes..."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":3484928,"phase":"W1905A","message":"Well, you didn't seem to get much out of that little drama."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":3491711,"phase":"W1905A","message":"Not my time."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":3497301,"phase":"W1905A","message":"You could do a good job in Greece if you would like to help me punish the Pope..."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":3501538,"phase":"W1905A","message":"A last ditch revenge plot? Potentially interested."},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":3509920,"phase":"W1905A","message":"Murderous rage here in Vienna... I mean, the Pope of all people. I am also hearing rumours that His Holiness is a Her in drag. What sacrilege! Down with all Popery I say."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":3510020,"phase":"W1905A","message":"Well, what can I say?<br \/><br \/>How about: death to the Italians! They have had it easy so far and an Italian win here is not good news for the Competition as a whole - she will get better countries to play with later."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":3510093,"phase":"W1905A","message":"I am trying to get Russia to cooperate with a removal of the Italians here...<br \/><br \/>The logic? Italy should not be allowed to snatch a good result, she has better countries to come. I am happy to invest my resources in stopping her. My only price, survival."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":3510126,"phase":"W1905A","message":"My Christmas card list is somewhat smaller now."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3538143,"phase":"W1905A","message":"he just told me it was \"payback\" for you not providing a support you sid you would provide? a bit stupid, if you ask me. <br \/><br \/>what are you thinking for this coming turn. i am trying to decide between building a fleet and army, or two armies. part of that depends on how you want to proceed with austria and turkey."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3552041,"phase":"W1905A","message":"Payback...how did he know I wasnt going to support him. Actually if you look at the orders, I supported the wrong unit...but he had support. <br \/><br \/>I am thinking eliminate Austria. Still not sure how to deal with Turkey."},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":3556422,"phase":"W1905A","message":"lol"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3560850,"phase":"W1905A","message":"we're on the same page - i just am not sure how you want o proceed with austria. i guess it depends in part on what he does with disbands. i guess i will leave the land battle to you - we'll need another fleet to tackle turkey properly, so i'll build one of each."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":3580529,"phase":"W1905A","message":"What has Germany offered you? I think I can do better."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":3628984,"phase":"W1905A","message":"I'm sure you can, he isn't real good about keeping friends."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":3631979,"phase":"W1905A","message":"I will attack Sweden from Norway and move the Norweigian Sea to Norway. You can attack Denmark. If you bounce him in Denmark then I can assist you in the fall. If he holds Sweden, then perhaps you can assist me in the fall. Regardless, I will not attack or support attacks against you. Norway can also assist you in holding the North Sea while you attack the Netherlands. If you have other ideas, I am open to them. I guess we need to see where he disbands."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":3634614,"phase":"W1905A","message":"You pretty much already laid out my intents. His disband will be interesting though."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":3635344,"phase":"W1905A","message":"I'll take a stab at Greece, if you're still offering support."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":3635381,"phase":"W1905A","message":"Are we still looking to move me to Greece?"},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":3636477,"phase":"W1905A","message":"OK...I will see what I can co-ordinate with Turkey. Personally I prefer to play as Italy, than Austria."}]},{"name":"S1906M","state":{"timestamp":1537459323065344,"zobrist_hash":"4973165506783578811","note":"","name":"S1906M","units":{"AUSTRIA":["A BOH","A BUD","A VEN"],"ENGLAND":["F EDI","A YOR","F NTH"],"FRANCE":["A BRE","A PAR"],"GERMANY":["A BEL","A MUN","A BUR","A LVN","F SWE","A PIC","F HOL"],"ITALY":["F GRE","F ENG","F TRI","A SPA","F MAO","A TYR","F NAP","A ROM"],"RUSSIA":["A FIN","F NWY","F BLA","A BUL","F NWG","A SER","A RUM","A MOS"],"TURKEY":["A ANK","A CON","F AEG"]},"centers":{"AUSTRIA":["BUD","VIE","VEN"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","PAR"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE"],"ITALY":["NAP","ROM","TUN","MAR","POR","TRI","GRE","SPA"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["GAL","BOH","VIE","BUD","ALB","VEN"],"ENGLAND":["LVP","SKA","EDI","LON","YOR","NTH"],"FRANCE":["BRE","GAS","PAR"],"GERMANY":["BER","KIE","MUN","BEL","HEL","BAL","RUH","BUR","DEN","LVN","SWE","PIC","HOL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","ION","WES","APU","POR","MAR","PIE","GRE","ENG","TRI","SPA","MAO","TYR"],"RUSSIA":["MOS","STP","BOT","FIN","WAR","BAR","NWY","BLA","UKR","BUL","NWG","SEV","SER","RUM"],"TURKEY":["EAS","ANK","SMY","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BOH - VIE","A BUD S A BOH - VIE","A VEN - TYR"],"ENGLAND":["F EDI - YOR","A YOR - WAL","F NTH - DEN"],"FRANCE":["A BRE - WAL VIA","A PAR H"],"GERMANY":["A BEL H","A MUN S A VEN - TYR","A BUR S A PIC - PAR","A LVN - MOS","F SWE - DEN","A PIC - PAR","F HOL S A BEL"],"ITALY":["A TYR - PIE","F TRI - VEN","A SPA - GAS","F ENG C A BRE - WAL","F GRE S F NAP - ION","F MAO S A SPA - GAS","F NAP - ION","A ROM S F TRI - VEN"],"RUSSIA":["A FIN S F NWY - SWE","F NWY - SWE","F BLA S A BUL","A BUL S A CON - GRE","F NWG - NWY","A SER - BUD","A RUM S A SER - BUD","A MOS H"],"TURKEY":["A ANK S A CON","A CON S A ANK","F AEG - GRE"]},"results":{"A BOH":[],"A BUD":["cut","dislodged"],"A VEN":[],"F EDI":["bounce"],"A YOR":["bounce"],"F NTH":["bounce"],"A BRE":["bounce"],"A PAR":["dislodged"],"A BEL":[],"A MUN":[],"A BUR":[],"A LVN":["bounce"],"F SWE":["bounce","dislodged"],"A PIC":[],"F HOL":[],"F GRE":["cut"],"F ENG":[],"F TRI":[],"A SPA":[],"F MAO":[],"A TYR":[],"F NAP":[],"A ROM":[],"A FIN":[],"F NWY":[],"F BLA":[],"A BUL":["void"],"F NWG":[],"A SER":[],"A RUM":[],"A MOS":[],"A ANK":[],"A CON":[],"F AEG":["bounce"]},"messages":[{"sender":"FRANCE","recipient":"ITALY","time_sent":3643522,"phase":"S1906M","message":"Anything I can do to postpone my elimination?"},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":3708302,"phase":"S1906M","message":"Any suggestions regarding our possible cooperation?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":3708510,"phase":"S1906M","message":"I suggest that we demilitarize the north. I await your specific suggestions."},{"sender":"GERMANY","recipient":"GLOBAL","time_sent":3710338,"phase":"S1906M","message":"This is weird. My computer not allowing me to enter moves without finalizing. Ergo, I am forced to. Be not dismayed. I am still negotiating."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":3710751,"phase":"S1906M","message":"Agreed. I will help England to remove you from there."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3720076,"phase":"S1906M","message":"what are you looking at for this turn. seems like you might have a newly minted Russo-English alliance on your hands? what are you hearing from each of them? i'll poke around and see what i can find out.<br \/><br \/>if you hit paris and i hit brest, we're sure to take out france's remaining SCs - that would be good, no?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":3720160,"phase":"S1906M","message":"well it looks like you may have a new ally in russia? that german stab was kind of pathetic. or maybe, since you've got germany over barrel, you can get him to help you? i am thinking i might just use this turn to finish france, but if you have need of my assistance, let me know."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3720178,"phase":"S1906M","message":"possibly. let me see what the options are. any suggestions from you?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":3720198,"phase":"S1906M","message":"so what are you thinking for this turn?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3720311,"phase":"S1906M","message":"ok so will you hit budapest? i'll work on retaking venice, but budapest is guaranteed yours. i'd like to see that you get vienna as well, but i guess that depends slightly on what austria does this turn - i imagine he'll focus more on me, letting you waltz in there. my mistake of forcing tyr is really going to cost me, and it gies you big gains. thought you have the obnoxious german to worry about too. what are you hearing from him these days?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3720948,"phase":"S1906M","message":"Germany refused to disband Livonia, so our relationship has gone south big time. I am working on repairing things with England."},{"sender":"FRANCE","recipient":"ITALY","time_sent":3722725,"phase":"S1906M","message":"I be your janissary? I could move on Germany, or you could even convoy me to England."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3722811,"phase":"S1906M","message":"ah, yes, i just realized that."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3722833,"phase":"S1906M","message":"funny, i was thinking that about england. i doubt he would see that coming! haha."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3724876,"phase":"S1906M","message":"i'm terribly sorry about that stab, and especially sorry that i bungled it so poorly. though i guess it certainly gives you another lease on life, having a unit behind my lines. i did enjoy working together with you, and who knows, maybe there's a way to work things out still..."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3724975,"phase":"S1906M","message":"what do you want to do about turkey for this turn?<br \/><br \/>and are you going to take budapest this turn? that is a factor in my decision of what moves to make. i am on good terms with england, i think, so if there's anyway i can confirm info for you or anything else useful, let me know..."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3726553,"phase":"S1906M","message":"Not with me it seems"},{"sender":"GERMANY","recipient":"ITALY","time_sent":3728979,"phase":"S1906M","message":"Indeed. Good idea."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":3729005,"phase":"S1906M","message":"Good luck."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3729485,"phase":"S1906M","message":"meanwhile i'm planning to vacate tyrolia, is that ok? or would you rather it help munich in some way? it sounds like russia is trying to make amends with england - so you may want to do some lobbying on your own behalf..."},{"sender":"TURKEY","recipient":"ITALY","time_sent":3740610,"phase":"S1906M","message":"I don't know. I could try for Bul, but I'm not sure how successful that would be."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":3740970,"phase":"S1906M","message":"I need to cover Vienna and would like to see Tyrolia stopped from giving any support, so I suppose he first move would be against Tyr."},{"sender":"ITALY","recipient":"TURKEY","time_sent":3751722,"phase":"S1906M","message":"right. it might work though. i'm waiting to hear what russia is planning."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":3753363,"phase":"S1906M","message":"Yes....where from? Constantinople?"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":3757038,"phase":"S1906M","message":"Aegean."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":3757991,"phase":"S1906M","message":"Ok. I have your orders for the Aegean. Let me just say that you can be confident in that, since there are few armies in the area, there can be no retreats inland. So it works well for me as well. Hopefully this will be a good stepping stone to expand. I am hopeful you will build fleets and we can both continue West."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":3758401,"phase":"S1906M","message":"That would be my plan."},{"sender":"GERMANY","recipient":"ITALY","time_sent":3790543,"phase":"S1906M","message":"You can help Mun by not attacking it. Russia, England, and I are at war. <br \/><br \/>Pray for Russia. He\/she has cancer of the Livonia."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":3790738,"phase":"S1906M","message":"I will not order an evacuation of Mun at this time. That would be a sucker move. I will, however, consider supporting an initiative of yours in the autumn.<br \/><br \/>Let's see what happens this spring."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3791229,"phase":"S1906M","message":"haha well i certainly won't be attacking munich, that would serve absolutely no purpose."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3794389,"phase":"S1906M","message":"i'm concerned that i haven't heard from you. i am going to put in provisional orders, but if there are specific things you want to see me do, let me know. heads up that turkey may attack you."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":3794967,"phase":"S1906M","message":"I am trying to evacuate from Bohemia... my Venetian army is hungry for some action, so I am hitting Tyrolia. If you could support that, I can get three units on Trieste for the Autumn and get back into some sort of defensive order."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":3795043,"phase":"S1906M","message":"No problem. I am happy to accept the hand of fate... Mind you, revenge is attractive as well... ; )"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":3795195,"phase":"S1906M","message":"of course it is... i would expect nothing less."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":3795531,"phase":"S1906M","message":"I am trying to stop the Italians and would appreciate being left breathing space here... If that is achieved, I will certainly act as the front line for you."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3797382,"phase":"S1906M","message":"Turkey cannot touch me. I will support hold BUL. I am not worried about him at this point. I am taking Budapest. Did you need a specific move from me?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3797750,"phase":"S1906M","message":"no that's fine. yes, as long as you cover bul, you're all set. <br \/><br \/>i'm moving tyr-pie, so it won't be there to help with vie, i'm afraid. i need to deal with this unit behind my lines - i can't believe how stupid i was to let this happen. <br \/><br \/>i'm trying to pry info out of germany, but no luck so far."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3797829,"phase":"S1906M","message":"i'll get fleets into the mix to help finish off turkey."},{"sender":"FRANCE","recipient":"ITALY","time_sent":3812055,"phase":"S1906M","message":"So what shall we do?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":3812504,"phase":"S1906M","message":"i'm not sure. messing with england only helps germany, really. but it sure would be unexpected, i suppose."},{"sender":"FRANCE","recipient":"ITALY","time_sent":3813174,"phase":"S1906M","message":"Well if you give me free leave, I can mess with Germany."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3813268,"phase":"S1906M","message":"i think unfortunately that i'm going to have to move against you, i'm sorry."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3813301,"phase":"S1906M","message":"if i hadn't completely mangled my stab on austria, i would be able to work with you, i'd have more options, but as it is, i have way too many weak spots to cover."},{"sender":"FRANCE","recipient":"ITALY","time_sent":3814685,"phase":"S1906M","message":"Hm, I would have thought that with the mangled stab you would be more likely to work with me.<br \/><br \/>Can you convoy Brest to Wales while taking Brest?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":3814909,"phase":"S1906M","message":"i thought of that, but it leaves my defenses still a bit thin. particularly if, as i assume he will, germany runs you out of paris."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3814941,"phase":"S1906M","message":"i'm more than happy to work with you, i just can't convoy, take brest, and cover marseilles properly."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3814959,"phase":"S1906M","message":"and spain"},{"sender":"FRANCE","recipient":"ITALY","time_sent":3815222,"phase":"S1906M","message":"You're no fun. =P"},{"sender":"ITALY","recipient":"FRANCE","time_sent":3815309,"phase":"S1906M","message":"i'm trying to see how it could work, hold on. and don't make baseless accusations! i am to a whole ton of fun!! =P"},{"sender":"ITALY","recipient":"FRANCE","time_sent":3815384,"phase":"S1906M","message":"ok, i see how it can work. i will convoy you to wales if you would like me to."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3815397,"phase":"S1906M","message":"i just wasn't thinking out of the box properly."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3815426,"phase":"S1906M","message":"what are you going to do with paris though? just give it up?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":3815529,"phase":"S1906M","message":"i'm going to help you some more. the french want me to convoy the army at brest to wales. that will be a pain for england, which is good for you, no?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":3815539,"phase":"S1906M","message":"That's much better! =DDD<br \/><br \/>But yes, I think Paris is a lost cause. If you have any requests for it, let me know. Otherwise it will hold it's position valiantly against superior forces, for glory and extra pages in the history textbook."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":3815551,"phase":"S1906M","message":"what's your plan here this turn? shall we coordinate in any way?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":3815695,"phase":"S1906M","message":"Twice I believe my message to you didn't get posted. I need a new modem lol, it loses connection far too often.<br \/><br \/>Russia is surprisingly not interested in help against Germany when I'm obviously not the threat right now and Germany not surprisingly stopped responding to me lol"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":3815726,"phase":"S1906M","message":"I'm just hoping to slip into Denmark, other than that, I just want to liiive! lol"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":3815938,"phase":"S1906M","message":"interesting. russia told me he did want your help. huh."},{"sender":"FRANCE","recipient":"ITALY","time_sent":3816402,"phase":"S1906M","message":"So is a convoy in order? I've got my tickets and I'm very excited to vacation outside the country."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3816549,"phase":"S1906M","message":"yup, please proceed to waiting area for boarding."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3820054,"phase":"S1906M","message":"what's your approach to england? do you mind if i mess with him a bit?"}]},{"name":"S1906R","state":{"timestamp":1537459323068028,"zobrist_hash":"5377801549511906515","note":"","name":"S1906R","units":{"AUSTRIA":["A VIE","A TYR","*A BUD"],"ENGLAND":["F EDI","A YOR","F NTH"],"FRANCE":["A BRE"],"GERMANY":["A BEL","A MUN","A BUR","A LVN","F HOL","A PAR","*F SWE"],"ITALY":["F GRE","F ENG","F MAO","A ROM","F VEN","A GAS","A PIE","F ION"],"RUSSIA":["A FIN","F BLA","A BUL","A RUM","A MOS","F SWE","F NWY","A BUD"],"TURKEY":["A ANK","A CON","F AEG"]},"centers":{"AUSTRIA":["BUD","VIE","VEN"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","PAR"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE"],"ITALY":["NAP","ROM","TUN","MAR","POR","TRI","GRE","SPA"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["GAL","BOH","ALB","VIE","TYR"],"ENGLAND":["LVP","SKA","EDI","LON","YOR","NTH"],"FRANCE":["BRE"],"GERMANY":["BER","KIE","MUN","BEL","HEL","BAL","RUH","BUR","DEN","LVN","PIC","HOL","PAR"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","MAR","GRE","ENG","TRI","SPA","MAO","VEN","GAS","PIE","ION"],"RUSSIA":["MOS","STP","BOT","FIN","WAR","BAR","BLA","UKR","BUL","NWG","SEV","SER","RUM","SWE","NWY","BUD"],"TURKEY":["EAS","ANK","SMY","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD R TRI"],"ENGLAND":[],"FRANCE":[],"GERMANY":["F SWE R SKA"],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A BUD":[],"F SWE":[],"A PAR":["disband"]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":3820776,"phase":"S1906R","message":"huh. interesting turn."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":3820844,"phase":"S1906R","message":"sorry about that sillyness. i won't be messing with you again - i need to be sure to take brest."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3820903,"phase":"S1906R","message":"especially interesting your obnoxious assistance to the turk."},{"sender":"ITALY","recipient":"TURKEY","time_sent":3820942,"phase":"S1906R","message":"well that was silly. you couldn't even coordinate moves properly with the russian?<br \/><br \/>shall i actually support you into bulgaria this turn?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":3821784,"phase":"S1906R","message":"Poop. That would have been cool."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3822807,"phase":"S1906R","message":"I figure obnoxious assistance is better than effective assistance. ;)"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":3827374,"phase":"S1906R","message":"Oh well, Death in Venice. I am, I believe buggered there."},{"sender":"ITALY","recipient":"FRANCE","time_sent":3827582,"phase":"S1906R","message":"ack. i thought about london. oh well."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3829915,"phase":"S1906R","message":"lol"},{"sender":"GERMANY","recipient":"ITALY","time_sent":3877562,"phase":"S1906R","message":"Ha ha. Absolutely. <br \/>btw. I made an error with Mun. I meant to hold. sorry."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3878360,"phase":"S1906R","message":"yeah, some error. a major error in judgement."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3878392,"phase":"S1906R","message":"but it's ok, it didn't matter. and i'm glad to know where i stand with you."},{"sender":"GERMANY","recipient":"ITALY","time_sent":3880043,"phase":"S1906R","message":"No, I'm serious. It was just a mistake. Let me make it up to you. What would you like me to do for you this term? It's the autumn, so if Austria thinks that we are fighting, then I can really repay you for Paris.<br \/><br \/>I consider you to be a staunch ally, and I hope that you feel the same way.<br \/><br \/>Please let me know what you want me to do."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3881915,"phase":"S1906R","message":"i don't know what to think. such mistakes are usually not. and there were a couple of them this turn. <br \/><br \/>but i'll take you at your word, as i think we do both benefit from working together. support into brest would be helpful, though i don't really need it - but as a gesture of good-will. otherwise, i'm not sure, to be honest."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3881942,"phase":"S1906R","message":"it will probably be pointless, but would you consider supporting me into trieste?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3881994,"phase":"S1906R","message":"actually, it is pointless, barring some crazy retreat to galicia by austria. so i'll figure out another plan."},{"sender":"ITALY","recipient":"GERMANY","time_sent":3882476,"phase":"S1906R","message":"basically i'd like to see you focus your energies on russia. it looks like you might lose two SCs this turn, so that's rough, but i can try to engage england once france is out of the way, so that you can get some breathing room."}]},{"name":"F1906M","state":{"timestamp":1537459323080864,"zobrist_hash":"1143651038134362135","note":"","name":"F1906M","units":{"AUSTRIA":["A VIE","A TYR","A TRI"],"ENGLAND":["F EDI","A YOR","F NTH"],"FRANCE":["A BRE"],"GERMANY":["A BEL","A MUN","A BUR","A LVN","F HOL","A PAR","F SKA"],"ITALY":["F GRE","F ENG","F MAO","A ROM","F VEN","A GAS","A PIE","F ION"],"RUSSIA":["A FIN","F BLA","A BUL","A RUM","A MOS","F SWE","F NWY","A BUD"],"TURKEY":["A ANK","A CON","F AEG"]},"centers":{"AUSTRIA":["BUD","VIE","VEN"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","PAR"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE"],"ITALY":["NAP","ROM","TUN","MAR","POR","TRI","GRE","SPA"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["GAL","BOH","ALB","VIE","TYR","TRI"],"ENGLAND":["LVP","EDI","LON","YOR","NTH"],"FRANCE":["BRE"],"GERMANY":["BER","KIE","MUN","BEL","HEL","BAL","RUH","BUR","DEN","LVN","PIC","HOL","PAR","SKA"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","MAR","GRE","ENG","SPA","MAO","VEN","GAS","PIE","ION"],"RUSSIA":["MOS","STP","BOT","FIN","WAR","BAR","BLA","UKR","BUL","NWG","SEV","SER","RUM","SWE","NWY","BUD"],"TURKEY":["EAS","ANK","SMY","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A TYR S A TRI","A VIE - BUD","A TRI S A VIE - BUD"],"ENGLAND":["F EDI - CLY","A YOR - LON","F NTH - DEN"],"FRANCE":["A BRE H"],"GERMANY":["A BEL H","A MUN - SIL","A BUR - MUN","A LVN - MOS","F HOL - NTH","A PAR S F MAO - BRE","F SKA - DEN"],"ITALY":["F ENG S F MAO - BRE","F GRE S F ION","F MAO - BRE","A ROM - VEN","A GAS - MAR","F ION S F GRE","A PIE S A ROM - VEN","F VEN - ADR"],"RUSSIA":["A FIN - STP","F BLA S A BUL","A BUL H","A RUM S A BUD","A MOS H","A BUD H","F SWE S F NTH - DEN","F NWY S A FIN - STP"],"TURKEY":["A ANK S A CON","A CON S A RUM - BUL","F AEG - BUL\/SC"]},"results":{"A VIE":["bounce"],"A TYR":[],"A TRI":[],"F EDI":[],"A YOR":[],"F NTH":[],"A BRE":["dislodged"],"A BEL":[],"A MUN":[],"A BUR":[],"A LVN":["bounce"],"F HOL":[],"A PAR":[],"F SKA":["bounce"],"F GRE":[],"F ENG":[],"F MAO":[],"A ROM":[],"F VEN":[],"A GAS":[],"A PIE":[],"F ION":[],"A FIN":[],"F BLA":[],"A BUL":[],"A RUM":[],"A MOS":[],"F SWE":[],"F NWY":[],"A BUD":[],"A ANK":[],"A CON":["void"],"F AEG":["bounce"]},"messages":[{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":4004094,"phase":"F1906M","message":"Which unit can I support into DEN?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":4012890,"phase":"F1906M","message":"North Sea, I knew Italy would have fun in Wales lol so this turn I'm gonna counter another aggressive move I see her doing. It seems this game will look like a wintergreen soon enough which is a fun combo."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":4013145,"phase":"F1906M","message":"lol it wasn't silly if I figured such moves would happen ^_^ When I'm good enough to be able to play assuming other players will do the best moves is when I'll have more fun in this game.<br \/><br \/>Embarrassed to admit but I still have the occasional ruling I'm not aware of which would help with the tactical part of the game. Until then, I take the diplomatic approach very light-hearted until I can be as precise as my movements."},{"sender":"GERMANY","recipient":"ITALY","time_sent":4053723,"phase":"F1906M","message":"I am assuming that you are attacking Bre from Mid. I will support that order."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":4056721,"phase":"F1906M","message":"it's all good. i'm leaving you be now, to finish off monsieur chrisp."},{"sender":"ITALY","recipient":"GERMANY","time_sent":4056769,"phase":"F1906M","message":"ok, great, thanks!"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":4090413,"phase":"F1906M","message":"So, will you support AEGEAN to GRE this turn?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":4090457,"phase":"F1906M","message":"Yes. Russian is not trustworthy. Not that I'm looking much better at this stage of the game, but really, I need to survive."},{"sender":"ITALY","recipient":"TURKEY","time_sent":4093853,"phase":"F1906M","message":"alright. i'm waiting to hear back about russia's plans so i can plan accordingly. i'll let you know."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4093886,"phase":"F1906M","message":"so i guess you are just consolidating your position?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4093899,"phase":"F1906M","message":"do you want to do anything about T?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4094647,"phase":"F1906M","message":"He's asking for help again with Greece. I hate to keep stringing the poor guy along....why don't you force him out of the Aegean."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4147986,"phase":"F1906M","message":"I have BUD supporting VEN-TRI....whether you use it or not."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4148041,"phase":"F1906M","message":"Sorry...forget that last message....I have to cancel that support."}]},{"name":"F1906R","state":{"timestamp":1537459323083022,"zobrist_hash":"1415370945470929709","note":"","name":"F1906R","units":{"AUSTRIA":["A VIE","A TYR","A TRI"],"ENGLAND":["F CLY","A LON","F DEN"],"FRANCE":["*A BRE"],"GERMANY":["A BEL","A LVN","A PAR","F SKA","A SIL","A MUN","F NTH"],"ITALY":["F GRE","F ENG","A PIE","F ION","F BRE","A VEN","F ADR","A MAR"],"RUSSIA":["F BLA","A BUL","A RUM","A MOS","F SWE","F NWY","A BUD","A STP"],"TURKEY":["A ANK","A CON","F AEG"]},"centers":{"AUSTRIA":["BUD","VIE","VEN"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","PAR"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL","SWE"],"ITALY":["NAP","ROM","TUN","MAR","POR","TRI","GRE","SPA"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["GAL","BOH","ALB","VIE","TYR","TRI"],"ENGLAND":["LVP","EDI","YOR","CLY","LON","DEN"],"FRANCE":[],"GERMANY":["BER","KIE","BEL","HEL","BAL","RUH","BUR","LVN","PIC","HOL","PAR","SKA","SIL","MUN","NTH"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","GRE","ENG","SPA","MAO","GAS","PIE","ION","BRE","VEN","ADR","MAR"],"RUSSIA":["MOS","BOT","FIN","WAR","BAR","BLA","UKR","BUL","NWG","SEV","SER","RUM","SWE","NWY","BUD","STP"],"TURKEY":["EAS","ANK","SMY","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["A BRE R GAS"],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A BRE":[]},"messages":[{"sender":"RUSSIA","recipient":"GERMANY","time_sent":4183411,"phase":"F1906R","message":"Did you want to demilitarize any other areas?"},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":4185932,"phase":"F1906R","message":"If you have any interest in repairing our friendship, I would ask you again to disband Livonia."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4186559,"phase":"F1906R","message":"As you can see....I knew I was leaving SER open, so I couldn't support you to TRI for fear of a retreat there. I think we quickly consolidating our grip on the board. I think we should start thinking how we are going to divide up things.<br \/><br \/>Germany refuses to yield on his attacks on me, so I can no longer consider a triple alliance with him.<br \/><br \/>England is still small and easy to contain. Germany's brashness has made it improbable that England will work with him again. But I think he will focus on Germany as well....so I think the goal is to keep them at each other's throats...while both are wedged between us.<br \/><br \/>Austria should go this year. I have no problem with you having both VIE and TRI if you feel you can hold them.<br \/><br \/>Turkey should go in the next two years....minimally I would want CON from Turkey, if not ANK as well.<br \/><br \/>If we can eliminate Austria and Turkey, then we can both wedge England and Germany from there.<br \/><br \/>Those are my thoughts. What do you think?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4199935,"phase":"F1906R","message":"You seem to be going after Turkey\/Russia... I can help you if that would let me survive."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":4229881,"phase":"F1906R","message":"OK. It's a deal."},{"sender":"TURKEY","recipient":"ITALY","time_sent":4232205,"phase":"F1906R","message":"well, that sucked.<br \/><br \/>I suspect some Russian agression now."}]},{"name":"W1906A","state":{"timestamp":1537459323085018,"zobrist_hash":"7224369982979116477","note":"","name":"W1906A","units":{"AUSTRIA":["A VIE","A TYR","A TRI"],"ENGLAND":["F CLY","A LON","F DEN"],"FRANCE":["A GAS"],"GERMANY":["A BEL","A LVN","A PAR","F SKA","A SIL","A MUN","F NTH"],"ITALY":["F GRE","F ENG","A PIE","F ION","F BRE","A VEN","F ADR","A MAR"],"RUSSIA":["F BLA","A BUL","A RUM","A MOS","F SWE","F NWY","A BUD","A STP"],"TURKEY":["A ANK","A CON","F AEG"]},"centers":{"AUSTRIA":["VIE","TRI"],"ENGLAND":["EDI","LON","LVP","DEN"],"FRANCE":[],"GERMANY":["BER","KIE","MUN","HOL","BEL","PAR"],"ITALY":["NAP","ROM","TUN","MAR","POR","GRE","SPA","VEN","BRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","BUD","SWE"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["GAL","BOH","ALB","VIE","TYR","TRI"],"ENGLAND":["LVP","EDI","YOR","CLY","LON","DEN"],"FRANCE":["GAS"],"GERMANY":["BER","KIE","BEL","HEL","BAL","RUH","BUR","LVN","PIC","HOL","PAR","SKA","SIL","MUN","NTH"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","GRE","ENG","SPA","MAO","PIE","ION","BRE","VEN","ADR","MAR"],"RUSSIA":["MOS","BOT","FIN","WAR","BAR","BLA","UKR","BUL","NWG","SEV","SER","RUM","SWE","NWY","BUD","STP"],"TURKEY":["EAS","ANK","SMY","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":-1,"homes":[]},"ENGLAND":{"count":1,"homes":["EDI","LVP"]},"FRANCE":{"count":-1,"homes":[]},"GERMANY":{"count":-1,"homes":[]},"ITALY":{"count":1,"homes":["NAP","ROM"]},"RUSSIA":{"count":2,"homes":["SEV","WAR"]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A TYR D"],"ENGLAND":["A LVP B"],"FRANCE":["A GAS D"],"GERMANY":["A LVN D"],"ITALY":["A ROM B"],"RUSSIA":["A SEV B","A WAR B"],"TURKEY":[]},"results":{"A TYR":[""],"A LVP":[""],"A GAS":[""],"A LVN":[""],"A ROM":[""],"A SEV":[""],"A WAR":[""]},"messages":[{"sender":"ITALY","recipient":"GLOBAL","time_sent":4327680,"phase":"W1906A","message":"my apologies for being MIA since Saturday. I was unexpectedly hospitalized. Now I am resting and spending time with love ones after a few scary days, but I will catch up on my press I promise."},{"sender":"TURKEY","recipient":"ITALY","time_sent":4328073,"phase":"W1906A","message":"I hope you're well."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4354489,"phase":"W1906A","message":"sorry, this all sounds good to me. more to come tomorrow, ok?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4355063,"phase":"W1906A","message":"Sure no problem...I wasn't holding up my end of the conversation, anyway ;)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4363333,"phase":"W1906A","message":"this all sounds good to me. you work with E against G, i can do vice versa, and we can finish T and AH. i would like tri, and vienna makes sense too. thoughts on builds?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4391752,"phase":"W1906A","message":"I am building armies this turn"},{"sender":"ITALY","recipient":"GERMANY","time_sent":4394549,"phase":"W1906A","message":"looks like you have to disband. what are you thinking you will do?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4394610,"phase":"W1906A","message":"i'm considering it."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4395138,"phase":"W1906A","message":"Take your time and consider well. I am sure it will help you to feel better after your recent ordeal... ; )"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4399664,"phase":"W1906A","message":"ok, so vienna will be hard for me to hold. if you take con and ank, that would give us a 12\/12 split, which seems good. my proposal is that you keep working with E against G, and i'll keep good with G and lightly harass E, while focusing on turkey at this point."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4399767,"phase":"W1906A","message":"so if i agreed to work with you, how would that change your disbands? what damage can you actually do to russia?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4401404,"phase":"W1906A","message":"I am disbanding Tyrolia as I need to keep my units together. I need to see an army in Armenia and then we can push East. I am happy to accept the role of shield and move forward as you follow behind. It means I will not get builds, I will just swap my SCs for Russian held ones."}]},{"name":"S1907M","state":{"timestamp":1537459323097555,"zobrist_hash":"8315952179945534142","note":"","name":"S1907M","units":{"AUSTRIA":["A VIE","A TRI"],"ENGLAND":["F CLY","A LON","F DEN","A LVP"],"FRANCE":[],"GERMANY":["A BEL","A PAR","F SKA","A SIL","A MUN","F NTH"],"ITALY":["F GRE","F ENG","A PIE","F ION","F BRE","A VEN","F ADR","A MAR","A ROM"],"RUSSIA":["F BLA","A BUL","A RUM","A MOS","F SWE","F NWY","A BUD","A STP","A SEV","A WAR"],"TURKEY":["A ANK","A CON","F AEG"]},"centers":{"AUSTRIA":["VIE","TRI"],"ENGLAND":["EDI","LON","LVP","DEN"],"FRANCE":[],"GERMANY":["BER","KIE","MUN","HOL","BEL","PAR"],"ITALY":["NAP","ROM","TUN","MAR","POR","GRE","SPA","VEN","BRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","BUD","SWE"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["GAL","BOH","ALB","VIE","TYR","TRI"],"ENGLAND":["LVP","EDI","YOR","CLY","LON","DEN"],"FRANCE":["GAS"],"GERMANY":["BER","KIE","BEL","HEL","BAL","RUH","BUR","LVN","PIC","HOL","PAR","SKA","SIL","MUN","NTH"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","GRE","ENG","SPA","MAO","PIE","ION","BRE","VEN","ADR","MAR"],"RUSSIA":["MOS","BOT","FIN","WAR","BAR","BLA","UKR","BUL","NWG","SEV","SER","RUM","SWE","NWY","BUD","STP"],"TURKEY":["EAS","ANK","SMY","CON","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE S A TRI - BUD","A TRI - BUD"],"ENGLAND":["F CLY - NWG","A LON H","F DEN H","A LVP - EDI"],"FRANCE":[],"GERMANY":["A BEL - PIC","A PAR - GAS","F SKA S F NTH - DEN","A SIL H","F NTH - DEN","A MUN - BUR"],"ITALY":["F ENG - IRI","F GRE S A CON - BUL","F ION S F GRE","A PIE - TYR","A VEN - ALB VIA","F ADR C A VEN - ALB","A MAR - GAS","F BRE - MAO","A ROM - VEN"],"RUSSIA":["F BLA S A SEV - ARM","A BUL S A BUD - SER","A RUM S A BUL","A MOS - WAR","A BUD - SER","F SWE S F DEN","F NWY S F SWE","A STP S F NWY","A SEV - ARM","A WAR - GAL"],"TURKEY":["A ANK - ARM","A CON - BUL","F AEG S A CON - BUL"]},"results":{"A VIE":[],"A TRI":[],"F CLY":[],"A LON":[],"F DEN":[],"A LVP":[],"A BEL":[],"A PAR":["bounce"],"F SKA":[],"A SIL":[],"A MUN":[],"F NTH":["bounce"],"F GRE":[],"F ENG":[],"A PIE":[],"F ION":[],"F BRE":[],"A VEN":[],"F ADR":[],"A MAR":["bounce"],"A ROM":[],"F BLA":[],"A BUL":["cut","dislodged"],"A RUM":[],"A MOS":[],"F SWE":[],"F NWY":[],"A BUD":[],"A STP":[],"A SEV":[],"A WAR":[],"A ANK":["bounce"],"A CON":[],"F AEG":[]},"messages":[{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":4440710,"phase":"S1907M","message":"Good game, France."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":4441117,"phase":"S1907M","message":"How can I help you this turn? Support hold DEN?<br \/>I am kinda surprised you didn't build a fleet. What's the plan with the armies? We need to get creative to increase your position."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":4445125,"phase":"S1907M","message":"The armies are a more flexible defense against Italy who now is bored enough to come my way lol. I would not mind the support I'll move Clyde out to Norwegian sea to help push his fleets back, we dont need him out and about in such cold waters."},{"sender":"TURKEY","recipient":"ITALY","time_sent":4477190,"phase":"S1907M","message":"So, where do we stand ? What are we going to do?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":4477215,"phase":"S1907M","message":"I'm pretty much toast, given the Army in Sev."},{"sender":"GERMANY","recipient":"ITALY","time_sent":4480726,"phase":"S1907M","message":"How do you want to approach Eng?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":4480749,"phase":"S1907M","message":"How do you want to approach Eng?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":4489727,"phase":"S1907M","message":"i'm open to suggestions. what's your initial thought?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4489974,"phase":"S1907M","message":"what's your game plan this year?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":4490030,"phase":"S1907M","message":"sorry about being absent.<br \/><br \/>at this point i have to decide if i am casting my lot with russia, or against him. i could still help you take bul, but you are right that the army at sev presents a problem."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4492169,"phase":"S1907M","message":"Cover Serbia...position for Turkey...pacify Germany...appear to be helpful to an English state that can't get off his island."},{"sender":"TURKEY","recipient":"ITALY","time_sent":4500705,"phase":"S1907M","message":"No worries about being away. Lots of things are more important than webDip.<br \/><br \/>You do need to make that choice. I hope you work with me. I think we can get Germany on board as well."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4500768,"phase":"S1907M","message":"haha, sounds good."},{"sender":"ITALY","recipient":"TURKEY","time_sent":4500809,"phase":"S1907M","message":"i think germany is already on board. <br \/><br \/>alright, that works. aeg-bul or con-bul?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4500939,"phase":"S1907M","message":"an army in armenia? i guess you'll have to take that up with the turk. but this works for me - are you trying for ser this turn?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":4501047,"phase":"S1907M","message":"how about i suppose your convoy into london? thought russia and england working together could dislodge you. do you want to just send your north sea fleet into london?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":4501076,"phase":"S1907M","message":"also can we keep bur, pic, and gas free of any additional units?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":4513665,"phase":"S1907M","message":"Con seems slightly better."},{"sender":"GERMANY","recipient":"ITALY","time_sent":4517121,"phase":"S1907M","message":"I don't mind the DMZs, but I need the North Sea. I'll get back to you tomorrow on the convoy. I'll sleep on it. Thanks for the offer."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4520028,"phase":"S1907M","message":"Sorry, I meant Albania. Obviously knackered when I posted that."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":4527511,"phase":"S1907M","message":"He left himself vulnerable by not building fleets. Convoy BEL to YOR?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":4569326,"phase":"S1907M","message":"Well, that would be easily countered."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":4571486,"phase":"S1907M","message":"Attack Gal, and I will set you up in War. What do you say?<br \/><br \/>Disbanding Liv was a smoke screen."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":4571666,"phase":"S1907M","message":"Italy and Russia are working together to squash us all. This term represents our only opportunity to stop them. I am attempting to coordinate with Turkey and Austria. What do you say? <br \/><br \/>I will support Den-Swe."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":4574177,"phase":"S1907M","message":"If I attack Galicia, I leave Vienna wide open. I am expecting to see an army enter Serbia this coming move, and that will be a real problem."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":4579022,"phase":"S1907M","message":"what's your game plan this year? germany has asked me for help against you."},{"sender":"ITALY","recipient":"GERMANY","time_sent":4579047,"phase":"S1907M","message":"let me know what you're thinking. deadline is coming up."},{"sender":"ITALY","recipient":"TURKEY","time_sent":4579061,"phase":"S1907M","message":"ok you have my support."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4579099,"phase":"S1907M","message":"perfect, that's what i was thinking as well. are you going to try for serbia? i think it's worth a shot."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":4579406,"phase":"S1907M","message":"lol he would, right now my only game plan it to survive and be of use."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":4579497,"phase":"S1907M","message":"I'm on a 7 hour car ride so I'll have response when I get back."},{"sender":"TURKEY","recipient":"ITALY","time_sent":4579887,"phase":"S1907M","message":"Thanks."},{"sender":"ITALY","recipient":"GERMANY","time_sent":4597281,"phase":"S1907M","message":"ok we've got 5 hours left, and i haven't heard from you. i'm ordering to irish and mao. i may get on once more before the end of the phase - let me know if you want me to support the convoy instead."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4604383,"phase":"S1907M","message":"I cannot risk losing Trieste - you are not in position to follow me into T if I get S because you should be moving to A!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4613337,"phase":"S1907M","message":"ok, i think we would be ok, but its your unit so as you please."}]},{"name":"F1907M","state":{"timestamp":1537459323111415,"zobrist_hash":"9180332589125385639","note":"","name":"F1907M","units":{"AUSTRIA":["A VIE","A BUD"],"ENGLAND":["A LON","F DEN","F NWG","A EDI"],"FRANCE":[],"GERMANY":["A PAR","F SKA","A SIL","F NTH","A PIC","A BUR"],"ITALY":["F GRE","F ION","F ADR","A MAR","F IRI","A TYR","F MAO","A ALB","A VEN"],"RUSSIA":["F BLA","A RUM","F SWE","F NWY","A STP","A WAR","A SER","A ARM","A GAL"],"TURKEY":["A ANK","F AEG","A BUL"]},"centers":{"AUSTRIA":["VIE","TRI"],"ENGLAND":["EDI","LON","LVP","DEN"],"FRANCE":[],"GERMANY":["BER","KIE","MUN","HOL","BEL","PAR"],"ITALY":["NAP","ROM","TUN","MAR","POR","GRE","SPA","VEN","BRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","BUD","SWE"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE","TRI","BUD"],"ENGLAND":["LVP","YOR","CLY","LON","DEN","NWG","EDI"],"FRANCE":["GAS"],"GERMANY":["BER","KIE","BEL","HEL","BAL","RUH","LVN","HOL","PAR","SKA","SIL","MUN","NTH","PIC","BUR"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","GRE","ENG","SPA","PIE","ION","BRE","ADR","MAR","IRI","TYR","MAO","ALB","VEN"],"RUSSIA":["MOS","BOT","FIN","BAR","BLA","UKR","SEV","RUM","SWE","NWY","STP","WAR","SER","ARM","GAL"],"TURKEY":["EAS","ANK","SMY","CON","AEG","BUL"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE - GAL","A BUD - RUM"],"ENGLAND":["A LON - YOR","F DEN S F NWG - NTH","F NWG - NTH","A EDI - YOR"],"FRANCE":[],"GERMANY":["A PAR S A PIC - BRE","F SKA S F NTH - DEN","A SIL - MUN","F NTH - DEN","A BUR - MAR","A PIC - BRE"],"ITALY":["F GRE - AEG","F ION - EAS","F ADR - ION","A MAR S F MAO - GAS","A VEN - PIE","A TYR - MUN","A ALB - TRI","F MAO - GAS","F IRI - ENG"],"RUSSIA":["F BLA S A ARM - ANK","A RUM - BUL","F SWE S F NWY","F NWY S F NWG - NTH","A STP S F NWY","A ARM - ANK","A GAL - RUM","A WAR H","A SER S A RUM - BUL"],"TURKEY":["A ANK H","F AEG - SMY","A BUL S A ALB - SER"]},"results":{"A VIE":["bounce"],"A BUD":["bounce"],"A LON":["bounce"],"F DEN":["cut","dislodged"],"F NWG":[],"A EDI":["bounce"],"A PAR":[],"F SKA":[],"A SIL":["bounce"],"F NTH":[],"A PIC":[],"A BUR":["bounce"],"F GRE":[],"F ION":[],"F ADR":[],"A MAR":["cut"],"F IRI":[],"A TYR":["bounce"],"F MAO":[],"A ALB":[],"A VEN":[],"F BLA":[],"A RUM":[],"F SWE":[],"F NWY":[],"A STP":[],"A WAR":[],"A SER":[],"A ARM":[],"A GAL":["bounce"],"A ANK":["dislodged"],"F AEG":[],"A BUL":["void","dislodged"]},"messages":[{"sender":"ITALY","recipient":"GERMANY","time_sent":4759336,"phase":"F1907M","message":"well i'm pretty sad about this turn of events. i thought we could really go the distance together. as you can see, i even stabbed russia this turn to help you out. but i might have to make up with him if you car really going to keep attacking me. <br \/><br \/>is there anyway we can work this out without you taking an SC from me?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4759383,"phase":"F1907M","message":"i guess so. <br \/><br \/>well it seems like with my help you can easily recover from this situation. are you up for working together again, or do you need a turn to see my intentions or something?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":4759392,"phase":"F1907M","message":"any news?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4759405,"phase":"F1907M","message":"any news?"},{"sender":"ITALY","recipient":"TURKEY","time_sent":4759436,"phase":"F1907M","message":"would you be able to use bul to support alb-ser?i can support hold at bul with greece."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4761764,"phase":"F1907M","message":"I believe we can still work together. Though, it's always hard to completely ignore that the immediate past turn didn't happen. If I didn't believe that you have to concerned about losses to Germany, it would make it more difficult. Unfortunately, I don't see how I can reclaim both of my lost territories this turn."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4776349,"phase":"F1907M","message":"No, I am hoping for some plans to be leaked."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4782278,"phase":"F1907M","message":"are you talking to germany? i'm interested in what he is saying. i'm talking with russia about making up, so hoping to learn something about his plans. he said he thinks he can't retake both his centers, so that is interesting."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4799310,"phase":"F1907M","message":"He can only take both with help."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4827196,"phase":"F1907M","message":"right, but he also has ank for sure. so that offets the loss."},{"sender":"ITALY","recipient":"GERMANY","time_sent":4827227,"phase":"F1907M","message":"the silent treatment? please, say it ain't so..."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4827523,"phase":"F1907M","message":"well you can certainly offset one with the gain of ank. and i can help you take the other. or at least keep it out of turkish hands. after thinking on this quite a bit, i have realized that i need to fight germany full force. that means i can't be having a war with you. <br \/><br \/>so i can take trieste, you take ank. you can retake bud with a short nudge from me. then, i could help you take bul (and you get a build) or you could help me take vie (and i get a build). the advantage to eliminating austria-hungary has me leaning toward asking for your support to vienna, but the advantage of currying your favor has me leaning toward offering support for you into bul. let me know your thoughts, and anything you think i am missing, or a better approach."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":4827670,"phase":"F1907M","message":"well, i think you can be of immense use, and likely end up as the third player in the draw b\/c of germany's behavior. <br \/><br \/>i send my profuse apologies for moving into your waters this last turn - it was clearly a mistake on so many level. i'll be moving to recover brest, so no need to worry about me trying for lvp. i would love to see germany get pushed back by you and russia working together, with my support as well. let me know how we can coordinate."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4827719,"phase":"F1907M","message":"i just realized supporting you into bul leaves a gap for austria to retreat to, so what do you think of helping me take vienna? of course i am open to other ideas and suggestions."},{"sender":"GERMANY","recipient":"ITALY","time_sent":4830232,"phase":"F1907M","message":"It isn't so. Unfortunately, I was tied up yesterday. Sorry.<br \/><br \/>I did tell you that Brest was mine, so it should be no surprise. You need to deal with that if we are going to work together."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":4830281,"phase":"F1907M","message":"I offered you survival. <br \/><br \/>Pity."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":4830418,"phase":"F1907M","message":"We can work together to fight off Russia and Italy.<br \/><br \/>They ARE allied, you know......."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":4830535,"phase":"F1907M","message":"This autumn, you could have Swe, and I, Denmark. It can still happen. I will support Den to Swe, and order Nth to Den. You only need to attack Nor and Swe."},{"sender":"ITALY","recipient":"GERMANY","time_sent":4830541,"phase":"F1907M","message":"glad to hear it about the silent thing. <br \/><br \/>well, i think if we are working together, communication needs to be the norm. your sudden move against my units and SCs without any communication does not encourage trust or alliance. i would have been glad to give you brest once i picked up another SC to compensate for it - and hand it over peacefully to you.<br \/><br \/>we also agreed to DMZs which you then violated. so, as far as i am concerned our days of working together have ended this turn, unless you want to offer me something to keep working with you. for this turn, that something would have to be letting me have brest and backing off.<br \/><br \/>you no doubt noticed that i stabbed russia this past turn. i did that so you could get so respite from him, while pushing back england. however, russia is just as distrustful of you as i now am, and he is willing to overlook this turn if i help him out to make up for the damage done. that includes the two of us parceling you up. i had hoped to see this game resolve in a 3 way draw with you, russia, and myself, but with you acting this way, i don't think you'll be the third one in the draw. why not change course now, so we can actually keep working together?"},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":4830568,"phase":"F1907M","message":"I am trying to survive here..."},{"sender":"GERMANY","recipient":"ITALY","time_sent":4830683,"phase":"F1907M","message":"Well I'm a blonde, so I believe you, but I wouldn't go telling that story to any brunettes."},{"sender":"ITALY","recipient":"GERMANY","time_sent":4830727,"phase":"F1907M","message":"sorry?"},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":4856030,"phase":"F1907M","message":"Anything from Turkey?"},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":4856049,"phase":"F1907M","message":"Anything from Turkey?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4857108,"phase":"F1907M","message":"Any idea what Turkey plans to do this turn? Has he said how he will defend against me?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":4857163,"phase":"F1907M","message":"Yes, I can do that."},{"sender":"TURKEY","recipient":"AUSTRIA","time_sent":4857237,"phase":"F1907M","message":"But if I stay in Ank, he for sure won't get Ank or Smy. Moving to Con is a gamble."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4857677,"phase":"F1907M","message":"i think you've got ank safely, and he might use aeg to cover con, let me find out."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":4863100,"phase":"F1907M","message":"Yes, he is talking about moving to Const."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":4863356,"phase":"F1907M","message":"I am supporting Norwegian Sea into North Sea with Denmark if that works for you. I also want to move Denmark to Kiel so that I can support you into Denmark from there and cut him down a little. What do you think?"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":4863668,"phase":"F1907M","message":"Last time you said that you didn't, and I knew you wouldn't. I know they are at least theirs is a more profitable one than yours."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":4864714,"phase":"F1907M","message":"If he tries the same moves then he will dislodge you from Denmark with a retreat to Kiel. So I think the North sea is most important so you can start attacking the Netherlands."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":4864793,"phase":"F1907M","message":"From Aegean? Or from Ank?"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":4865189,"phase":"F1907M","message":"Ank. But he is wavering."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":4865822,"phase":"F1907M","message":"lol Ms. Bent, no need to play coy with me. After my use is run out this wintergreen will merge in my direction, more than likely leaving me green because Turkey gets the purple ^_^"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":4879965,"phase":"F1907M","message":"you see this ending in a two way draw? not likely. i don't trust russia enough for that, and i'm sure he doesn't trust me enough. so we need a third party, if you don't want to be it, i guess we are left with germany..."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4880024,"phase":"F1907M","message":"since we are getting close to end of phase, can we firm up our plan? i'd like to know what i should do with greece and tyr. <br \/><br \/>thanks!"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4880162,"phase":"F1907M","message":"I will not assist you into VIE this turn...Ideally, I would like GRE to go to AEG, but mostly, it would help if you didn't assist the other nations."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4880489,"phase":"F1907M","message":"ok that is all fine. i can't leave greece empty yet, sorry, but will be glad to once we have a more militarized balkans area. let me know what other moves you would want to see from me to build confidence in working with you - aside from not helping AH or T."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4914069,"phase":"F1907M","message":"I am relying on you bouncing in T this turn. I remain hopeful... ; )"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4914101,"phase":"F1907M","message":"you got it!"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":4914109,"phase":"F1907M","message":"I don't get that tactic at all"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":4914244,"phase":"F1907M","message":"what tactic? <br \/><br \/>2 way draws are very unstable. it's super easy for one person to stab the other for the win. on the other hand, 3 ways draws are stable b\/c if one person goes for the solo, the other two will stop him\/her."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4915246,"phase":"F1907M","message":"but are you taking bud? should i cut support there? can i get you to move against germany?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4915317,"phase":"F1907M","message":"actually scratch that, i need tyr for something else, sorry. i can help you with bul if needed though."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":4917002,"phase":"F1907M","message":"I understand that, but your record indicates wins not draws :P"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":4917024,"phase":"F1907M","message":"I never said I wasn't going to work with you and Russia, I have been."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":4917459,"phase":"F1907M","message":"check out my record hon, i have far more draws than wins =)<br \/><br \/>glad to hear you're on board."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4917690,"phase":"F1907M","message":"Thank you. A friend in need etc etc..."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":4917893,"phase":"F1907M","message":"Maybe I should have checked prior lol. I was just going by your wonderful Ghostrating, I was unaware draws were that valuable."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":4918231,"phase":"F1907M","message":"depends on who is in the game when you draw. for you, achieving a draw in this game should help your rating quite a bit."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":4918587,"phase":"F1907M","message":"I was distant most of this month so my 170 is acceptable to me. ^_^ Honestly I play for fun, not points or rating. I've joined games with 1 SC (where no one would expect it and I had options to tease other players) which doubled my number of losses but it was fun to stir up some games. (I didn't like that their strategy and movements were dependent on the CD and someone not coming back)"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4919321,"phase":"F1907M","message":"That's why I was preferring you to bump the Aegean."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":4919574,"phase":"F1907M","message":"i agree, having fun is the most important thing =)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4919590,"phase":"F1907M","message":"ok, i can do that."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4920129,"phase":"F1907M","message":"i will be taking trieste, so please don't take a stab at it. i have ordered to cut support at aeg."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4920151,"phase":"F1907M","message":"are you going to move against germany at all?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4920585,"phase":"F1907M","message":"I will be assisting England into the North Sea. I want to secure my south before a land offensive on Germany."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4920975,"phase":"F1907M","message":"ok, that is completely reasonable."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":4923121,"phase":"F1907M","message":"Was that last message in English?"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":4924345,"phase":"F1907M","message":"It is my second language but absolutely. Just lacks enough punctuation for your to figure it out apparently. You've said nothing but obvious things to me and lies, why would I care about the welfare of someone who is not able to contribute?"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":4924367,"phase":"F1907M","message":"*you instead of your. Again, I don't like English."}]},{"name":"F1907R","state":{"timestamp":1537459323114723,"zobrist_hash":"489498198074197764","note":"","name":"F1907R","units":{"AUSTRIA":["A VIE","A BUD"],"ENGLAND":["A LON","A EDI","F NTH","*F DEN"],"FRANCE":[],"GERMANY":["A PAR","F SKA","A SIL","A BUR","F DEN","A BRE"],"ITALY":["A MAR","A TYR","F AEG","F EAS","F ION","F ENG","F GAS","A TRI","A PIE"],"RUSSIA":["F BLA","F SWE","F NWY","A STP","A WAR","A SER","A GAL","A BUL","A ANK"],"TURKEY":["F SMY","*A ANK","*A BUL"]},"centers":{"AUSTRIA":["VIE","TRI"],"ENGLAND":["EDI","LON","LVP","DEN"],"FRANCE":[],"GERMANY":["BER","KIE","MUN","HOL","BEL","PAR"],"ITALY":["NAP","ROM","TUN","MAR","POR","GRE","SPA","VEN","BRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","BUD","SWE"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE","BUD"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","NTH"],"FRANCE":[],"GERMANY":["BER","KIE","BEL","HEL","BAL","RUH","LVN","HOL","PAR","SKA","SIL","MUN","PIC","BUR","DEN","BRE"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","GRE","SPA","ADR","MAR","IRI","TYR","MAO","ALB","VEN","AEG","EAS","ION","ENG","GAS","TRI","PIE"],"RUSSIA":["MOS","BOT","FIN","BAR","BLA","UKR","SEV","RUM","SWE","NWY","STP","WAR","SER","ARM","GAL","BUL","ANK"],"TURKEY":["CON","SMY"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F DEN R KIE"],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":["A ANK R CON","A BUL R GRE"]},"results":{"F DEN":[],"A ANK":[],"A BUL":[]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":4927055,"phase":"F1907R","message":"ah shoot. i didn't expect that at all. grrr."},{"sender":"TURKEY","recipient":"ITALY","time_sent":4927206,"phase":"F1907R","message":"Not very nice."},{"sender":"ITALY","recipient":"TURKEY","time_sent":4927500,"phase":"F1907R","message":"apologies, with the german stab, i had to make nice with russia.<br \/><br \/>at least i messed up, so you get to benefit from that."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4927689,"phase":"F1907R","message":"i am sorry, i needed russia's help against germany, and to make up for my loss of brest."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4928621,"phase":"F1907R","message":"If you feel you are too overextended in Turkey, I can help you obtain SMY. Greece is safe."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4928747,"phase":"F1907R","message":"Crap...I see he will retreat to Greece."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4928807,"phase":"F1907R","message":"No matter...we can eliminate him this year....probably Austria too."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":4931632,"phase":"F1907R","message":"I tried to return Galicia back to the south. You didn't defend Trieste....so maybe you fell for Italy's siren's call :)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4935785,"phase":"F1907R","message":"yes i think we'll be ok. if i destroy the fleet at ion, can you support me into greece?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4936521,"phase":"F1907R","message":"Yes."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4936599,"phase":"F1907R","message":"great."},{"sender":"TURKEY","recipient":"ITALY","time_sent":4940278,"phase":"F1907R","message":"For now..."},{"sender":"TURKEY","recipient":"ITALY","time_sent":4940297,"phase":"F1907R","message":":(<br \/><br \/>If you take more of my centers, it means you like kicking puppies."},{"sender":"ITALY","recipient":"TURKEY","time_sent":4940406,"phase":"F1907R","message":"i will have to retake greece - but i don't like kicking puppies, so maybe i'll leave you smyrna. or you could not retreat to greece and we could be on fine terms. =)"},{"sender":"TURKEY","recipient":"ITALY","time_sent":4940613,"phase":"F1907R","message":"But if I don't retreat to Greece, I'm sure someone will start a thread on the forum about how I don't know how to play the game properly because I don't conform to their 'every game should draw because I say so' philosophy."},{"sender":"ITALY","recipient":"TURKEY","time_sent":4940673,"phase":"F1907R","message":"hahahahahahaha"},{"sender":"TURKEY","recipient":"ITALY","time_sent":4940844,"phase":"F1907R","message":"And really, I don't need that sort of thing!<br \/><br \/>Besides, I've got to play with Babak in my next game. \"Yay!\""},{"sender":"ITALY","recipient":"TURKEY","time_sent":4940976,"phase":"F1907R","message":"haha that cracked me up for real. i'm sitting here in my office laughing. <br \/><br \/>too true, too true. best to play hard to get!! =P"},{"sender":"TURKEY","recipient":"ITALY","time_sent":4941240,"phase":"F1907R","message":"Anyway, unforutnately, I must retreat to Greece. Our previous interactions have all fallen apart (we're both to blame for that), so I must take what I can get. Survival is my goal now (and we both can see that that won't last much longer). I must make it as hard on my enemies (which, seems to be everyone) as possible until I am slaughtered.<br \/><br \/>It was fun."},{"sender":"ITALY","recipient":"TURKEY","time_sent":4941714,"phase":"F1907R","message":"understood. i'm sorry it couldn't work out - i had every intention of honoring my commitment to you, until germany stabbed me. that forced my hand, please yell at him ;D"},{"sender":"TURKEY","recipient":"ITALY","time_sent":4942000,"phase":"F1907R","message":"it won't really do any good ;)"},{"sender":"ITALY","recipient":"TURKEY","time_sent":4942037,"phase":"F1907R","message":"oh i know, but it would make me happy..."}]},{"name":"W1907A","state":{"timestamp":1537459323116417,"zobrist_hash":"4481053256405504098","note":"","name":"W1907A","units":{"AUSTRIA":["A VIE","A BUD"],"ENGLAND":["A LON","A EDI","F NTH","F KIE"],"FRANCE":[],"GERMANY":["A PAR","F SKA","A SIL","A BUR","F DEN","A BRE"],"ITALY":["A MAR","A TYR","F AEG","F EAS","F ION","F ENG","F GAS","A TRI","A PIE"],"RUSSIA":["F BLA","F SWE","F NWY","A STP","A WAR","A SER","A GAL","A BUL","A ANK"],"TURKEY":["F SMY","A CON","A GRE"]},"centers":{"AUSTRIA":["VIE","BUD"],"ENGLAND":["EDI","LON","LVP","KIE"],"FRANCE":[],"GERMANY":["BER","MUN","HOL","BEL","PAR","DEN","BRE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","SWE","ANK"],"TURKEY":["CON","SMY","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE","BUD"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","NTH","KIE"],"FRANCE":[],"GERMANY":["BER","BEL","HEL","BAL","RUH","LVN","HOL","PAR","SKA","SIL","MUN","PIC","BUR","DEN","BRE"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","SPA","ADR","MAR","IRI","TYR","MAO","ALB","VEN","AEG","EAS","ION","ENG","GAS","TRI","PIE"],"RUSSIA":["MOS","BOT","FIN","BAR","BLA","UKR","SEV","RUM","SWE","NWY","STP","WAR","SER","ARM","GAL","BUL","ANK"],"TURKEY":["SMY","CON","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":1,"homes":["BER","MUN"]},"ITALY":{"count":-1,"homes":[]},"RUSSIA":{"count":1,"homes":["MOS","SEV"]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":["A MUN B"],"ITALY":["F ION D"],"RUSSIA":["A SEV B"],"TURKEY":[]},"results":{"A MUN":[""],"F ION":[""],"A SEV":[""]},"messages":[{"sender":"TURKEY","recipient":"GERMANY","time_sent":4942247,"phase":"W1907A","message":"Italy wants me to yell at you for stabbing her, which lead to her needing to play nice with Russia, which has more or less ensured that I have a broken back.<br \/><br \/>So consider this being yelled at.<br \/><br \/>Good luck with the rest of the game!"},{"sender":"TURKEY","recipient":"ITALY","time_sent":4942266,"phase":"W1907A","message":"Done."},{"sender":"ITALY","recipient":"TURKEY","time_sent":4942476,"phase":"W1907A","message":"lol"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":4943867,"phase":"W1907A","message":"Mercy?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":4945876,"phase":"W1907A","message":"Hey...sorry for not engaging you more this game. I started feeling bad because I knew I would be lying to you and it was less painful to just stop communicating with you. Unfortunately, I need your land."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":4947327,"phase":"W1907A","message":"I think you are a very good player, btw. I would to play with you again."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":4947744,"phase":"W1907A","message":"Fair enough."},{"sender":"GERMANY","recipient":"TURKEY","time_sent":4947784,"phase":"W1907A","message":"Thank you. It's never over 'till it's over."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":4947814,"phase":"W1907A","message":"What language do you speak at home?"},{"sender":"TURKEY","recipient":"GERMANY","time_sent":4948902,"phase":"W1907A","message":"I'll be doing my best, but I fear the writing is on the wall."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":4949078,"phase":"W1907A","message":"You should have trusted me to be a good ally.<br \/><br \/>So I could have stabbed you. ;)"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4952229,"phase":"W1907A","message":"You are somewhat one-dimensional... It is a shame I only get to meet you as Austria."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":4952291,"phase":"W1907A","message":"Unfortunately, I was in a rather tight spot. Her diplomatic skills are limited: 'lie + aologise'."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":4952564,"phase":"W1907A","message":"Can I expect more interference in the Balkans this coming year?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4956281,"phase":"W1907A","message":"how so?"}]},{"name":"S1908M","state":{"timestamp":1537459323128325,"zobrist_hash":"2729492704594640180","note":"","name":"S1908M","units":{"AUSTRIA":["A VIE","A BUD"],"ENGLAND":["A LON","A EDI","F NTH","F KIE"],"FRANCE":[],"GERMANY":["A PAR","F SKA","A SIL","A BUR","F DEN","A BRE","A MUN"],"ITALY":["A MAR","A TYR","F AEG","F EAS","F ENG","F GAS","A TRI","A PIE"],"RUSSIA":["F BLA","F SWE","F NWY","A STP","A WAR","A SER","A GAL","A BUL","A ANK","A SEV"],"TURKEY":["F SMY","A CON","A GRE"]},"centers":{"AUSTRIA":["VIE","BUD"],"ENGLAND":["EDI","LON","LVP","KIE"],"FRANCE":[],"GERMANY":["BER","MUN","HOL","BEL","PAR","DEN","BRE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","SWE","ANK"],"TURKEY":["CON","SMY","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE","BUD"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","NTH","KIE"],"FRANCE":[],"GERMANY":["BER","BEL","HEL","BAL","RUH","LVN","HOL","PAR","SKA","SIL","MUN","PIC","BUR","DEN","BRE"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","SPA","ADR","MAR","IRI","TYR","MAO","ALB","VEN","AEG","EAS","ION","ENG","GAS","TRI","PIE"],"RUSSIA":["MOS","BOT","FIN","BAR","BLA","UKR","SEV","RUM","SWE","NWY","STP","WAR","SER","ARM","GAL","BUL","ANK"],"TURKEY":["SMY","CON","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE S A BUD - GAL","A BUD - GAL"],"ENGLAND":["A LON H","A EDI - BEL VIA","F NTH C A EDI - BEL","F KIE - DEN"],"FRANCE":[],"GERMANY":["A PAR - GAS","F SKA S F DEN - SWE","A SIL - MUN","A BUR S A PAR - GAS","A BRE S A PAR - GAS","F DEN - SWE","A MUN - RUH"],"ITALY":["A MAR - SPA","A TYR - VIE","A PIE - MAR","F AEG - GRE","A TRI S A GAL - BUD","F ENG S A EDI - BEL","F GAS - MAO","F EAS - SMY"],"RUSSIA":["F BLA - CON","F SWE - DEN","F NWY - SKA","A STP - FIN","A GAL - BUD","A WAR H","A SER S F AEG - GRE","A BUL S F AEG - GRE","A ANK S F BLA - CON","A SEV - RUM"],"TURKEY":["F SMY S A CON","A GRE - BUL","A CON S F SMY"]},"results":{"A VIE":["cut"],"A BUD":["bounce","dislodged"],"A LON":[],"A EDI":[],"F NTH":[],"F KIE":["bounce"],"A PAR":[],"F SKA":["cut"],"A SIL":[],"A BUR":[],"F DEN":["bounce"],"A BRE":[],"A MUN":[],"A MAR":[],"A TYR":["bounce"],"F AEG":[],"F EAS":["bounce"],"F ENG":[],"F GAS":[],"A TRI":[],"A PIE":[],"F BLA":[],"F SWE":["bounce"],"F NWY":["bounce"],"A STP":[],"A WAR":[],"A SER":[],"A GAL":[],"A BUL":[],"A ANK":[],"A SEV":[],"F SMY":["cut"],"A CON":["cut","dislodged"],"A GRE":["bounce","dislodged"]},"messages":[{"sender":"RUSSIA","recipient":"ITALY","time_sent":4958957,"phase":"S1908M","message":"I will support AEG-GRE....I need EAS-SMY so that I can take CON from BLK. Then in the fall, I will support you to SMY. That will completely eliminate Turkey."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4959148,"phase":"S1908M","message":"sounds like a plan. and we can do the same with austria - i can support myself into vienna, can you support yourself into budapest?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4959375,"phase":"S1908M","message":"If possible. I am hitting BUD from GAL in the event that Austria and Turkey are working to prolong each other. I am trying to get SEV to RUM....but it is unsupported."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4960105,"phase":"S1908M","message":"ok, i can hit vie from tyr - maybe i should support hold at ser? is that helpful?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4960414,"phase":"S1908M","message":"Actually...if you wanted...you could hit VIE and support GAL-BUD. He will be unable to retreat...and will have to disband....making VIE easy picking in the fall. That will give you SMY, GRE and VIE and me CON and BUD. Not a bad turn and we eliminate the variables :)"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":4960691,"phase":"S1908M","message":"I am going to try to disband SKA. I would suggest leaving HOL open for a retreat, though a retreat to the Baltic Sea isn't a bad one. The other option is I can support a convoy to DEN...I can bump SKA and support the move to DEN. That probably is the better option."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4961181,"phase":"S1908M","message":"sounds good to me =)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4961207,"phase":"S1908M","message":"then we mop up germany, and call it game? is that how you see things progressing?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4962022,"phase":"S1908M","message":"I do have a large percentages of draws ;)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":4963781,"phase":"S1908M","message":"why... so do i!!! ;D"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4964900,"phase":"S1908M","message":"I do not believe we play each other again in this competition."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":4964934,"phase":"S1908M","message":"No. I have to teach the lady a lesson in manners... You have freedom to act as you will."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4965339,"phase":"S1908M","message":"sorry, yes, you are right. i meant: how so? in response to your comment that i am one-dimensional - how am i one dimensional?"},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":5000170,"phase":"S1908M","message":"Do you want to try some sort of Hail Mary move here?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5001086,"phase":"S1908M","message":"shall i support a convoy from edi to bel? that would be a good thing, to get one of your armies on land =)<br \/><br \/>i'll ask russia to support your hold at nth, so germany can't dislodge you."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5001140,"phase":"S1908M","message":"ok, supporting gal-bud, tapping smy, aeg-gre expecting your support (please =D )<br \/><br \/>also, can norway support hold at nth? i'd like to support a convoy from edi to bel. what do you think?"},{"sender":"TURKEY","recipient":"ITALY","time_sent":5001472,"phase":"S1908M","message":"I could support Trieste into Serbia, if you want to make that move."},{"sender":"TURKEY","recipient":"ITALY","time_sent":5001523,"phase":"S1908M","message":"Heck, I'd support Aeg to Bul too!<br \/><br \/>In return, you'd leave Smy for another day."},{"sender":"ITALY","recipient":"TURKEY","time_sent":5001544,"phase":"S1908M","message":"i appreciate the offer, but i think i've settled into clean-up mode."},{"sender":"TURKEY","recipient":"ITALY","time_sent":5001859,"phase":"S1908M","message":"I reject your rejection. :P"},{"sender":"ITALY","recipient":"TURKEY","time_sent":5001966,"phase":"S1908M","message":"=P"},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":5002712,"phase":"S1908M","message":"It sounds good.. What is it?"},{"sender":"TURKEY","recipient":"GLOBAL","time_sent":5003259,"phase":"S1908M","message":"Continued attacks on Turkish holdings are forbidden."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":5003346,"phase":"S1908M","message":"Bribes are required for compliance..."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":5003438,"phase":"S1908M","message":"graft is outlawed in the italian imperium."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":5004483,"phase":"S1908M","message":"Fine, I'll take a double share."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5004643,"phase":"S1908M","message":"England is my girl...she's only got eyes for big poppa! Go whore somewhere else. <br \/><br \/>J\/K.....if England requests it I'll do it."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5004674,"phase":"S1908M","message":"alright, waiting to here back from him about it. =)"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5016464,"phase":"S1908M","message":"I would not mind a convoy from Edi to Bel."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5016562,"phase":"S1908M","message":"I was thinking a convoy to Belgium would be easier because I'm not sure how long Kiel will stay if any longer."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":5016579,"phase":"S1908M","message":"ASL (American Sign Language)"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5017191,"phase":"S1908M","message":"russia said he would support hold at nth, if you ask him to."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5018254,"phase":"S1908M","message":"I asked, and the convoy should go through as long as it doesn't bounce in Belgium."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5018282,"phase":"S1908M","message":"So what do you think of convoy to Belgium and Kiel hitting Denmark?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5018400,"phase":"S1908M","message":"with support from me, it will not bounce =)"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5019995,"phase":"S1908M","message":"Sure...Did you want me to support you there? What can I do to help? My units are at your disposal."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5020091,"phase":"S1908M","message":"I'm sure just a support hold in North Sea would let it go through. Germany is fighting but will soon have little to fight with."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5032880,"phase":"S1908M","message":"Ok...But Kiel has to hit Denmark."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5043137,"phase":"S1908M","message":"Yeah, I have kiel going to Denmark"},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":5085149,"phase":"S1908M","message":"Attack Gal from Bud with support. Let's try to get you into Russia."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":5085475,"phase":"S1908M","message":"A new home?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5085514,"phase":"S1908M","message":"What are your intentions now? I need a good answer to make me work with you after recent reverses..."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":5085538,"phase":"S1908M","message":"Can you stop that woman? Please tell me how..."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5085636,"phase":"S1908M","message":"my intention is to work with russia to eliminate you and turkey. sorry."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5086368,"phase":"S1908M","message":"Now, as this is DIplomacy, I must assume that you are lying. Therefore you are going to help me, and I must say thank you."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":5086415,"phase":"S1908M","message":"Italy has just told me that she and Russia will eliminate me this turn. A nice message."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5086644,"phase":"S1908M","message":"LOL<br \/><br \/>damn you! you saw straight through my deceit ;D"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5086673,"phase":"S1908M","message":"shall we finalize and see where this turn takes us?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5092693,"phase":"S1908M","message":"Yes, sorry - no orders placed yet."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5092932,"phase":"S1908M","message":"no worries."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5093009,"phase":"S1908M","message":"Well, I pushed the button - why the delay?"}]},{"name":"S1908R","state":{"timestamp":1537459323131101,"zobrist_hash":"4531806272637490787","note":"","name":"S1908R","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A LON","F NTH","F KIE","A BEL"],"FRANCE":[],"GERMANY":["F SKA","A BUR","F DEN","A BRE","A GAS","A MUN","A RUH"],"ITALY":["A TYR","F EAS","F ENG","A TRI","A SPA","F GRE","F MAO","A MAR"],"RUSSIA":["F SWE","F NWY","A WAR","A SER","A BUL","A ANK","F CON","A FIN","A BUD","A RUM"],"TURKEY":["F SMY","*A GRE"]},"centers":{"AUSTRIA":["VIE","BUD"],"ENGLAND":["EDI","LON","LVP","KIE"],"FRANCE":[],"GERMANY":["BER","MUN","HOL","BEL","PAR","DEN","BRE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","SWE","ANK"],"TURKEY":["CON","SMY","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","NTH","KIE","BEL"],"FRANCE":[],"GERMANY":["BER","HEL","BAL","LVN","HOL","PAR","SKA","SIL","PIC","BUR","DEN","BRE","GAS","MUN","RUH"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","ALB","VEN","AEG","EAS","ION","ENG","TRI","PIE","SPA","GRE","MAO","MAR"],"RUSSIA":["MOS","BOT","BAR","BLA","UKR","SEV","SWE","NWY","STP","WAR","SER","ARM","GAL","BUL","ANK","CON","FIN","BUD","RUM"],"TURKEY":["SMY"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":["A GRE R ALB"]},"results":{"A GRE":[],"A CON":["disband"],"A BUD":["disband"]},"messages":[{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5093091,"phase":"S1908R","message":"Cruel world..."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":5093123,"phase":"S1908R","message":"I am getting a sinking feeling here..."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5093155,"phase":"S1908R","message":"that went well. nicely done. looks like germany is leaving the backdoor open for you ;D"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5093198,"phase":"S1908R","message":"it waits until the next 5 minute mark (11:15. 11:20, etc) to process<br \/><br \/>and it is a cruel world, i'm sorry things couldn't work out."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5093368,"phase":"S1908R","message":"i can support hold at belgium, and you could convoy london to holland =)<br \/><br \/>or wait, either way. i'd just like to see the german get his come-uppance quickly ;D"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5093442,"phase":"S1908R","message":"so i'll get your support into smyrna this turn, yes? and i'll take vienna. i am encouraging another convoy by england, into holland, and i can support his hold at belgium. we should have the german on his knees shortly."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5093522,"phase":"S1908R","message":"also, we need to keep a close eye on russia - he'll be at 12 SCs after this turn, and can easily nab a few from me. if he starts to make a move for a solo, we have to stop him, ok?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5093647,"phase":"S1908R","message":"can i also ask that you back off a bit from the south east, maybe send bul-rum and rum-gal or rum-ukr? and maybe next turn move con or ank out of there?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5097242,"phase":"S1908R","message":"Absolutely. ^_^"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5097399,"phase":"S1908R","message":"perfect."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":5122083,"phase":"S1908R","message":"Any reason I shouldn't help Italy take your last center?"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":5124203,"phase":"S1908R","message":"Personal greed? No need to give her the extra advantage."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":5125808,"phase":"S1908R","message":"OK....hold your unit."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":5140271,"phase":"S1908R","message":"I will..."}]},{"name":"F1908M","state":{"timestamp":1537459323143059,"zobrist_hash":"1512148695271043964","note":"","name":"F1908M","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A LON","F NTH","F KIE","A BEL"],"FRANCE":[],"GERMANY":["F SKA","A BUR","F DEN","A BRE","A GAS","A MUN","A RUH"],"ITALY":["A TYR","F EAS","F ENG","A TRI","A SPA","F GRE","F MAO","A MAR"],"RUSSIA":["F SWE","F NWY","A WAR","A SER","A BUL","A ANK","F CON","A FIN","A BUD","A RUM"],"TURKEY":["F SMY","A ALB"]},"centers":{"AUSTRIA":["VIE","BUD"],"ENGLAND":["EDI","LON","LVP","KIE"],"FRANCE":[],"GERMANY":["BER","MUN","HOL","BEL","PAR","DEN","BRE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","SWE","ANK"],"TURKEY":["CON","SMY","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","NTH","KIE","BEL"],"FRANCE":[],"GERMANY":["BER","HEL","BAL","LVN","HOL","PAR","SKA","SIL","PIC","BUR","DEN","BRE","GAS","MUN","RUH"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","AEG","EAS","ION","ENG","TRI","PIE","SPA","GRE","MAO","MAR"],"RUSSIA":["MOS","BOT","BAR","BLA","UKR","SEV","SWE","NWY","STP","WAR","SER","ARM","GAL","BUL","ANK","CON","FIN","BUD","RUM"],"TURKEY":["SMY","ALB"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE - TYR"],"ENGLAND":["A LON - HOL VIA","F NTH C A LON - HOL","F KIE - BER","A BEL S A LON - HOL"],"FRANCE":[],"GERMANY":["F SKA - DEN","A BUR - BEL","A BRE H","F DEN - KIE","A MUN S A VIE - TYR","A RUH - HOL","A GAS S A BRE"],"ITALY":["A TYR - VIE","A TRI S A TYR - VIE","F ENG S A BEL","F EAS - SMY","A MAR S A SPA","A SPA S A MAR","F MAO - BRE","F GRE H"],"RUSSIA":["F SWE - BAL","F NWY S A FIN - SWE","A WAR - GAL","A SER S A BUL - GRE","A BUL - GRE","A ANK - SMY","A RUM S A BUD","A FIN - SWE","F CON S A ANK - SMY","A BUD S A VIE"],"TURKEY":["F SMY H","A ALB - SER"]},"results":{"A VIE":["bounce"],"A LON":["bounce"],"F NTH":[],"F KIE":[],"A BEL":["cut"],"F SKA":[],"A BUR":["bounce"],"F DEN":[],"A BRE":[],"A GAS":[],"A MUN":[],"A RUH":["bounce"],"A TYR":["bounce"],"F EAS":["bounce"],"F ENG":[],"A TRI":[],"A SPA":[],"F GRE":[],"F MAO":["bounce"],"A MAR":[],"F SWE":[],"F NWY":[],"A WAR":[],"A SER":["cut"],"A BUL":["bounce"],"A ANK":[],"F CON":[],"A FIN":[],"A BUD":["void"],"A RUM":[],"F SMY":["dislodged"],"A ALB":["bounce"]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":5172508,"phase":"F1908M","message":"alright, plan for this turn?"},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":5176335,"phase":"F1908M","message":"I will support Vie-Tyr. OK?"},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":5176743,"phase":"F1908M","message":"Okay, that sounds like the best option for me."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5188891,"phase":"F1908M","message":"I keep writing lengthy responses but they don't go through. I think I am taking too much time...so let me explain...no, let me sum up....<br \/><br \/>We should continue our original plan this year. I think we both want assurrances in the southeast so we need to discuss how to do that. I suggest that be our first item on next years agenda. I assume you will be taking VIE from Tyl since TRI is threatened by ALB."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5189061,"phase":"F1908M","message":"OK...how can I help you this turn?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5189925,"phase":"F1908M","message":"yes, tyl-vie is my plan. can ser support hold tri, and bud support tyl-vie?<br \/><br \/>i think we should start discussing assurances now - the moves we make now will have a bearing on what happens next. the only practical move for you to make for de-escalation this turn is to move Rum (preferable to Ukr, but i understand if you want to move to Gal) and then we can continue our DMZing of the southeast next year. i would want to see the fleet at Con go back to the Black Sea, and some futher redeployment of your armies to the northern front."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5195367,"phase":"F1908M","message":"Rumania is no issue...I will move that unit north. I don't see an advantage to me to move it to UKR, though. I can move CON to Black Sea in the spring, else he retreats there. Also BUL can retreat in the spring...in the event you take a stab at it, I can take it back the following fall. So other than RUM...I think these retreats are spring discussions."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5195771,"phase":"F1908M","message":"i agree completely, i just wanted to make sure we were on the same page. <br \/><br \/>to confirm, i have support for tyl-vie and eas-smy? and support hold for tri?<br \/><br \/>i have offered england support if he wants to convoy to holland (support hold at belgium). does that make sense, or do you see a better way forward?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5195979,"phase":"F1908M","message":"Do you have a guarantee that ALB isn't hitting TRI? Maybe Austria supports him to TRI? Just pondering the possibilities."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5196042,"phase":"F1908M","message":"I think this turn I'm more useful to stir chaos lol. I assumed you were hitting Ska and moving finland over or going to baltic and finland over. I'm sure he'll support Burgundy into Belgium and hit Kiel or bounce Berlin. Otherwise I'm not sure how he is going to pull off a recovery."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":5204644,"phase":"F1908M","message":"You might offer Turkey support to Trieste....doubtful that he gets it...since I expect Vienna's support to be cut....but you never know."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5210842,"phase":"F1908M","message":"yes but i'm hitting vie from tyl, so that support would be cut. which is why support from bud is needed."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5211212,"phase":"F1908M","message":"But won't Trieste's support also be cut...giving you no advance?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5216572,"phase":"F1908M","message":"Am I wrong?"},{"sender":"TURKEY","recipient":"GLOBAL","time_sent":5260404,"phase":"F1908M","message":"looks like I'm going to win the race (for second last!)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5268362,"phase":"F1908M","message":"if you support tyl-vie from bud, the move will be successful... no? my goal is to not lose tri, and gain vienna. that's why bud S tyl-vie and ser SH tri are moves i asked you to do - are you able to do them? does that make sense? or am i missing something?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5269640,"phase":"F1908M","message":"Ok...I see what I am missing. I was having SER support holding BUD.<br \/>I can do the moves you requested so that you can gain VIE but I will have RUM support holding BUD. We are still 4 nations in a very tight area."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5277515,"phase":"F1908M","message":"ok, that's fine have rum hold bud and rum can move in spring with the others. thanks for your help and cooperation - i think we are in good shape now."}]},{"name":"F1908R","state":{"timestamp":1537459323145237,"zobrist_hash":"1275764903810005135","note":"","name":"F1908R","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A LON","F NTH","A BEL","F BER"],"FRANCE":[],"GERMANY":["A BUR","A BRE","A GAS","A MUN","A RUH","F DEN","F KIE"],"ITALY":["A TYR","F EAS","F ENG","A TRI","A SPA","F GRE","F MAO","A MAR"],"RUSSIA":["F NWY","A SER","A BUL","F CON","A BUD","A RUM","F BAL","A GAL","A SMY","A SWE"],"TURKEY":["A ALB","*F SMY"]},"centers":{"AUSTRIA":["VIE","BUD"],"ENGLAND":["EDI","LON","LVP","KIE"],"FRANCE":[],"GERMANY":["BER","MUN","HOL","BEL","PAR","DEN","BRE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","SWE","ANK"],"TURKEY":["CON","SMY","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","NTH","BEL","BER"],"FRANCE":[],"GERMANY":["HEL","LVN","HOL","PAR","SKA","SIL","PIC","BUR","BRE","GAS","MUN","RUH","DEN","KIE"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","AEG","EAS","ION","ENG","TRI","PIE","SPA","GRE","MAO","MAR"],"RUSSIA":["MOS","BOT","BAR","BLA","UKR","SEV","NWY","STP","WAR","SER","ARM","BUL","ANK","CON","FIN","BUD","RUM","BAL","GAL","SMY","SWE"],"TURKEY":["ALB"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":["F SMY R AEG"]},"results":{"F SMY":[]},"messages":[]},{"name":"W1908A","state":{"timestamp":1537459323147627,"zobrist_hash":"77811525105833702","note":"","name":"W1908A","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A LON","F NTH","A BEL","F BER"],"FRANCE":[],"GERMANY":["A BUR","A BRE","A GAS","A MUN","A RUH","F DEN","F KIE"],"ITALY":["A TYR","F EAS","F ENG","A TRI","A SPA","F GRE","F MAO","A MAR"],"RUSSIA":["F NWY","A SER","A BUL","F CON","A BUD","A RUM","F BAL","A GAL","A SMY","A SWE"],"TURKEY":["A ALB","F AEG"]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP","BER","BEL"],"FRANCE":[],"GERMANY":["MUN","HOL","PAR","DEN","BRE","KIE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","SWE","ANK","BUD","CON","SMY"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","NTH","BEL","BER"],"FRANCE":[],"GERMANY":["HEL","LVN","HOL","PAR","SKA","SIL","PIC","BUR","BRE","GAS","MUN","RUH","DEN","KIE"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","EAS","ION","ENG","TRI","PIE","SPA","GRE","MAO","MAR"],"RUSSIA":["MOS","BOT","BAR","BLA","UKR","SEV","NWY","STP","WAR","SER","ARM","BUL","ANK","CON","FIN","BUD","RUM","BAL","GAL","SMY","SWE"],"TURKEY":["ALB","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":1,"homes":["EDI","LVP"]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":-1,"homes":[]},"ITALY":{"count":1,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":3,"homes":["MOS","SEV","STP","WAR"]},"TURKEY":{"count":-2,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI B"],"FRANCE":[],"GERMANY":["A GAS D"],"ITALY":["A VEN B"],"RUSSIA":["F SEV B","A STP B","A WAR B"],"TURKEY":["A ALB D","F AEG D"]},"results":{"F EDI":[""],"A GAS":[""],"A VEN":[""],"F SEV":[""],"A STP":[""],"A WAR":[""],"A ALB":[""],"F AEG":[""]},"messages":[{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":5348731,"phase":"W1908A","message":"Good game, Turkey."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":5348967,"phase":"W1908A","message":"Ha, I outlasted the Turk!"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":5349972,"phase":"W1908A","message":"Would survival be possible here as vassal state or mercenary army? I would like to see the Italians lose this game so that they profit very little from their (her) treachery. I am most willing to assist."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":5350336,"phase":"W1908A","message":"Yes....if you will work for me, I will promise do everything possible to get you a survive. You didn't hold as I asked last turn. I can't have that. One screw up and I am no longer bound to my promise."},{"sender":"TURKEY","recipient":"GLOBAL","time_sent":5350854,"phase":"W1908A","message":"I'll accept a draw! :) Offer's good till the end of this phase!"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5351596,"phase":"W1908A","message":"I expect I can get TRI and GRE....but I don't expect to get more than that. I have made a deal with Austria to keep him alive...so I won't get VIE. And with Italy's disappointment in the south, I can't help but think she will have to return her units to the Mediterranean.<br \/><br \/>So I want to really help you bulk up the next few years. DEN can be had with either an army or fleet this spring...with a very good chance of KIE in the fall."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5360048,"phase":"W1908A","message":"alright, with russia now on on course to get a solo, are you willing to stop your futile attacks on me and work against him?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5360133,"phase":"W1908A","message":"i had a pretty good feeling i would get stabbed by russia this turn- not as bad as i thought it might be though. so, you need to build a fleet and put some pressure on him. if possible we should get germany on our side, but he's enough of an idiot he just might let russia win. i hope not though."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5360211,"phase":"W1908A","message":"pretty much what i expected, but i thought you'd get two of my centers for sure. anyway, not much i could have done. you might well get lucky if mapleleaf doesn't quit attacking us."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5365723,"phase":"W1908A","message":"Such is life...I was always concerned about Turkey's move that turn. If it's any consolation, I don't expect to see the shores of Italy, but I do need to secure the Eastern Med."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5370331,"phase":"W1908A","message":"Absolutely. You're the one that has enabled Russia, not I."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5371182,"phase":"W1908A","message":"I'm surprised Italy didn't get Brest and move Spain to Gascony that turn."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5371282,"phase":"W1908A","message":"lol, I hope not. He is still trying to keep me on his side which I assume means he wants me to go after you soon. I don't plan on going after you at all."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5371348,"phase":"W1908A","message":"I'm all sorts of scattered this game. I wouldn't mind building up more against Germany as well so I can be useful instead of a slight tool as I'm forced to be currently lol"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5372446,"phase":"W1908A","message":"No...I think the move to Berlin was a great move...it got him out of position. You can put a pincher move on him....which for Mapleleaf has to be making him boil. DEN and KIE are easily yours...with a good shot at HOL."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":5380236,"phase":"W1908A","message":"Germany offered me a way to survive and it worked. However, I am more than willing to play the vassal here."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5393687,"phase":"W1908A","message":"I'm sure we can throw him in for a bigger loop."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5437716,"phase":"W1908A","message":"We need to demilitarize France, and I need you to help me versus England."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5437770,"phase":"W1908A","message":"what's your proposal for france? and wouldn't it be helpful to have england's assistance against russia?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5437822,"phase":"W1908A","message":"well, looks like you just got a new lease on life. i'll be supporting your hold so russia doesn't take vienna."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5437842,"phase":"W1908A","message":"what exactly do you mean by \"secure the eastern med\"?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5437876,"phase":"W1908A","message":"i am negotiating with germany now. trying to get him to hand over brest and paris to you in exchange for bel and berlin. or would you prefer other SCs?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5438356,"phase":"W1908A","message":"You have offered to marry me three times already... Will I get lucky this time round?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5438451,"phase":"W1908A","message":"heh heh. well, necessity is the mother of... something. russia is poised to steamroll the board (not surprisingly) so it's time to take counter measures."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5452664,"phase":"W1908A","message":"what are you going to disband? if it's something in france, i can move MAO to engage russia."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5453653,"phase":"W1908A","message":"You mean so that we can all start moving against Russia?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5453860,"phase":"W1908A","message":"yes. but let's see, i haven't heard back from him."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5524402,"phase":"W1908A","message":"I offered my assistance to England already. <br \/><br \/>Do you see the fleet in Berlin?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":5524623,"phase":"W1908A","message":"I will operate in good faith. If you cross me, then I will give the game to Russia. <br \/><br \/>Stop helping England."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5525116,"phase":"W1908A","message":"My number one goal is to stop Russia from winning. I think England's help is needed to stop Russia. You two need to stop fighting, or else Russia will win."}]},{"name":"S1909M","state":{"timestamp":1537459323160138,"zobrist_hash":"6125853724189954425","note":"","name":"S1909M","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A LON","F NTH","A BEL","F BER","F EDI"],"FRANCE":[],"GERMANY":["A BUR","A BRE","A MUN","A RUH","F DEN","F KIE"],"ITALY":["A TYR","F EAS","F ENG","A TRI","A SPA","F GRE","F MAO","A MAR","A VEN"],"RUSSIA":["F NWY","A SER","A BUL","F CON","A BUD","A RUM","F BAL","A GAL","A SMY","A SWE","F SEV","A STP","A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP","BER","BEL"],"FRANCE":[],"GERMANY":["MUN","HOL","PAR","DEN","BRE","KIE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","SWE","ANK","BUD","CON","SMY"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","NTH","BEL","BER"],"FRANCE":[],"GERMANY":["HEL","LVN","HOL","PAR","SKA","SIL","PIC","BUR","BRE","GAS","MUN","RUH","DEN","KIE"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","EAS","ION","ENG","TRI","PIE","SPA","GRE","MAO","MAR"],"RUSSIA":["MOS","BOT","BAR","BLA","UKR","SEV","NWY","STP","WAR","SER","ARM","BUL","ANK","CON","FIN","BUD","RUM","BAL","GAL","SMY","SWE"],"TURKEY":["ALB","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE S A SER - TRI"],"ENGLAND":["A LON H","F NTH - NWY","A BEL H","F BER H","F EDI - NTH"],"FRANCE":[],"GERMANY":["A BUR - BEL","A BRE H","A MUN - TYR","A RUH S A BUR - BEL","F KIE - HOL","F DEN - NTH"],"ITALY":["A TYR S A VIE","A TRI S A VIE","F ENG S A BEL","F EAS - AEG","A MAR - BUR","A SPA - GAS","F MAO S A SPA - GAS","F GRE H","A VEN S A TRI"],"RUSSIA":["F NWY - SKA","A SER - TRI","A BUL - GRE","A RUM - SER","F CON - SMY","A BUD S A SER - TRI","A SMY - SYR","A GAL S A WAR - SIL","A SWE - NWY","F BAL - KIE","F SEV - BLA","A WAR - SIL","A STP S F NTH - NWY"],"TURKEY":[]},"results":{"A VIE":[],"A LON":[],"F NTH":[],"A BEL":[],"F BER":[],"F EDI":["bounce"],"A BUR":["bounce"],"A BRE":[],"A MUN":["bounce"],"A RUH":[],"F DEN":["bounce"],"F KIE":[],"A TYR":["cut"],"F EAS":[],"F ENG":[],"A TRI":["cut","dislodged"],"A SPA":[],"F GRE":[],"F MAO":[],"A MAR":["bounce"],"A VEN":[],"F NWY":[],"A SER":[],"A BUL":["bounce"],"F CON":[],"A BUD":[],"A RUM":[],"F BAL":[],"A GAL":[],"A SMY":[],"A SWE":["bounce"],"F SEV":[],"A STP":[],"A WAR":[]},"messages":[{"sender":"ENGLAND","recipient":"ITALY","time_sent":5531480,"phase":"S1909M","message":"Still no word from Germany?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5531855,"phase":"S1909M","message":"he is demanding that i stop helping you. i'm trying to reason with him, but it's hard. this is going to be hard. i think you'll have to give up berlin - and maybe i can get him to trade bel for bre? i'll ask him. perhaps he would support you into norway in exchange for you vacating berlin - i'll ask him that as well."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5532146,"phase":"S1909M","message":"He still demands in this position? Balls on that one lol."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5532215,"phase":"S1909M","message":"So how are we going to stir Germany up some more for this turn?<br \/><br \/>He is really pissed that I'm in his area lol"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5532216,"phase":"S1909M","message":"well, we do need him to stop russia, so he kind of has us over a barrel."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5532268,"phase":"S1909M","message":"russia needs five SCs. he's got greece this year, and then with german cooperation could take berlin, den, kiel and vie or mun. germany is so crazy, i think he actually would help russia win."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5532375,"phase":"S1909M","message":"Alright, what about this? England trades you Belgium for Brest and vacates Berlin in exchange for support into Norway in autumn. what do you think? would that work for you? meanwhile, can i get you to move mun to boh, and shift your armies east? <br \/><br \/>i will garrison spain with the fleet at MAO, and figure out what unit i will disband when i lose greece (most likely fleet at EC). with greece, russia only needs four SCs.<br \/><br \/>let me know what you think. thanks."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5533331,"phase":"S1909M","message":"I am going to be focusing on Italy...I want to give you Norway, but I want to ensure you take it with a fleet only. I will send Norway to the Skaggerack and move Sweden to Norway....which will bounce your army....but I will have STP support NS to Norway...if you want it."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5534045,"phase":"S1909M","message":"Oh, that is an interesting way of preventing me from moving an army there. I like that ^_^ this is why I love playing with new and especially with strategy filled players. I'll move North Sea there and Edi back to North. what shall we do with the Baltic and Berlin?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5534262,"phase":"S1909M","message":"lol nice. Russia is offering me Norway for some reason. If you take Brest and move further on \"old France\" territory can't we get to the key spots and hold him off? Unless you are right and Germany is willing to roll over for Russia."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5536857,"phase":"S1909M","message":"I can hit KIE or support hold you...you can either get Italy to bump MUN or appease Germany for this turn...while I move to Silesia."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5537743,"phase":"S1909M","message":"germany told me he is willing to roll over for russia if i attack him. i'd rather not test that."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5539280,"phase":"S1909M","message":"lol wow, nice level of professionalism."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5539311,"phase":"S1909M","message":"i know =\/"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5539367,"phase":"S1909M","message":"Well Germany has spent the last year telling Italy he will only ally with her if they finish me off lol so he is the only one I care to move against."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5541036,"phase":"S1909M","message":"OK...so I will support hold you....but there is a chance DEN will bump me in the BAL....the only assurance you have is if Italy will bump MUN."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5541106,"phase":"S1909M","message":"Wait...even that won't help if he attacks from MUN. I would have to bump KIE and Italy would have to bump MUN."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":5556223,"phase":"S1909M","message":"Will Vienna support SER-TRI?"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":5556377,"phase":"S1909M","message":"Yes."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5556424,"phase":"S1909M","message":"Anything I can do here to win survival?"},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":5556467,"phase":"S1909M","message":"Can you hit Tyrolia?"},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":5556512,"phase":"S1909M","message":"Thanks."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":5556737,"phase":"S1909M","message":"I cannot do much but I hope that works. I have asked Germany to hit Tyrolia for me... I did not tell him why, but it would let me survive so he could be willing. If he does, you get Trieste."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5563017,"phase":"S1909M","message":"well i'm seeing if anything is useful besides holding your ground with my help. i think you can support munich into bohemia, but he might go to silesia instead. but i will support hold at vienna in any case."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5566828,"phase":"S1909M","message":"Much appreciated... One support or two? If you only support with Trieste, Russia will take me out."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5608176,"phase":"S1909M","message":"support from tyrolia."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5608946,"phase":"S1909M","message":"hey there, just wanting to coordinate our plans against russia. please let me know what you are thinking."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5613594,"phase":"S1909M","message":"England cannot be trusted. You are attempting to play me for a fool.<br \/><br \/>Bad idea."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5613672,"phase":"S1909M","message":"i am not trying to play you for a fool, and i think england can be trusted. i am trying to broker an agreement between you two, but you aren't talking to me at all about it. let me help you. <br \/><br \/>i take your threat to give the game to russia very seriously, and i am extremely concerned that russia not win this."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5613947,"phase":"S1909M","message":"You are helping England. England is attacking me. Do the math.<br \/><br \/>Until I SEE you attacking England, I will conduct myself accordingly.<br \/><br \/>There is no point in any further discussion."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5613988,"phase":"S1909M","message":"We do not need England at all.<br \/><br \/>You are lying to me."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5614159,"phase":"S1909M","message":"i am not going to attack england. i want his help against russia. i will attack russia and DMZ with you. i was working with england to fight you after YOU attacked ME. poor memory, or what? <br \/><br \/>i am now offering you peace in exchange for fighting russia together so he doesn't win. i think you and england can make peace if you want to, but i guess you don't want to. i'll leave you two to duke it out, but that only helps russia, so you are playing into his hands. <br \/><br \/>your accusations that i am lying are silly and baseless. i haven't lied to you the entire game, unlike you."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5614231,"phase":"S1909M","message":"what are you willing to do to make peace with germany? he is being really antagonistic to me right now."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5614249,"phase":"S1909M","message":"if you were willing to give up berlin, maybe he would chill the fuck out."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5614312,"phase":"S1909M","message":"well mapleleaf is being his typical whiny little bitch self, so you might well win this game. ugh, i dreaded it when i saw his name in my group."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5614347,"phase":"S1909M","message":"germany is being a whiny fool, insisting i attack england. can you get him to see some reason about the larger threat here?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":5614425,"phase":"S1909M","message":"and quit the curmudgeon act. <br \/><br \/>if you want me to keep attacking, keep it up. i'll gladly help russia dismember you if you refuse to join the team to stop him from soloing."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5616102,"phase":"S1909M","message":"He hasn't said anything to me! Not a single message, he is giving you all the crap not me."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5616452,"phase":"S1909M","message":"Good Ol' Mapleleaf....can't say he isn't consistent. Do you want to work with England and me to eliminate him?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5616899,"phase":"S1909M","message":"alright, i guess we have no choice. let's take him out. it means russia gets the game, but oh well. <br \/><br \/>can you support mar-bur? i'll support hold at bel again, and if you can try the convoy again, that would be good."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5617916,"phase":"S1909M","message":"i know, for real.<br \/><br \/>that is certainly an option. but it's undesirable, b\/c then you win the game =\/"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5618439,"phase":"S1909M","message":"But it is an option ;)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5618722,"phase":"S1909M","message":"sure thing. i guess it's worth a shot."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5618808,"phase":"S1909M","message":"russia has offered to work with me to take you out. i don't want to do that. i want to stop russia from winning. are you on board with that plan, or not?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5630974,"phase":"S1909M","message":"I put in the support. I'll assume you're supporting Spain to Gascony with MAO?<br \/><br \/>Russia seems sincere about wanting to keep Austria alive which means he will need the Denmark\/Kiel\/Berlin combo to win. Sadly he'll get greece and Trieste soon but I'm hoping by then we'll be in a better position to start pushing him back.<br \/><br \/>If Germany wants to work together I'm up for it but he is the kind of jackass who will only accept help and not give it. Or at least towards me in this game."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5631030,"phase":"S1909M","message":"I'm trying to get Italy to hit Munich but she hasn't been talking to me. I think she might be conspiring with Germany to try to hold you off and take me out :("},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5631279,"phase":"S1909M","message":"yes, he is being an idiot. it's so furstrating.<br \/><br \/>i am moving to gas, yes. i'll keep trying to get germany on our side, but if he won't, then there's no other option. meanwhile i'll do my best to hold back russia in the south. the quicker we can get into place against russia the better. i'm supporting austria's hold as well, so hopefully he will work with us too, and not russia."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5632522,"phase":"S1909M","message":"Everyone is telling me how no one is working together and how terrible things are....I am starting to believe that opposite may be true."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5646717,"phase":"S1909M","message":"Germany is still being a jerk to me needlessly lol so it's possible and since I'm not getting good communications with Italy you might be right. lol"},{"sender":"GERMANY","recipient":"ITALY","time_sent":5697292,"phase":"S1909M","message":"It's not an act.<br \/><br \/>I am a curmudgeon."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5697393,"phase":"S1909M","message":"You have a better chance versus Russia with me, rather than with England.<br \/><br \/>Your position makes no sense."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5697556,"phase":"S1909M","message":"haha alright then.<br \/><br \/>but we have a better chance of stopping russia with england's help. you want england to help russia? i don't think that's a good alternative. why can't you make peace with him? i am more than willing to help arrange it, and he's said he is glad to reach an agreement and work with you. england is much weaker than you, so that would seem like an ideal partnership. what is it that keeps you from working with him?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":5706390,"phase":"S1909M","message":"ok i haven't heard anything from you except an unwillingness to work with me. i'll have to move against you."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5706581,"phase":"S1909M","message":"alright, looks like we are on for taking down the uncooperative deutsch."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5706831,"phase":"S1909M","message":"Alright. Cool. Only a few minutes for this turn. Let's see what fall brings.<br \/><br \/>Why are these deals always made in the autumn? :P"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5707108,"phase":"S1909M","message":"heh heh."}]},{"name":"S1909R","state":{"timestamp":1537459323162474,"zobrist_hash":"8281908187422333951","note":"","name":"S1909R","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A LON","A BEL","F BER","F EDI","F NWY"],"FRANCE":[],"GERMANY":["A BUR","A BRE","A MUN","A RUH","F DEN","F HOL"],"ITALY":["A TYR","F ENG","F GRE","F MAO","A MAR","A VEN","F AEG","A GAS","*A TRI"],"RUSSIA":["A BUL","A BUD","A GAL","A SWE","A STP","F SKA","A TRI","F SMY","A SER","F KIE","A SYR","F BLA","A SIL"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP","BER","BEL"],"FRANCE":[],"GERMANY":["MUN","HOL","PAR","DEN","BRE","KIE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","SWE","ANK","BUD","CON","SMY"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","NTH","BEL","BER","NWY"],"FRANCE":[],"GERMANY":["HEL","LVN","PAR","PIC","BUR","BRE","MUN","RUH","DEN","HOL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","EAS","ION","ENG","PIE","SPA","GRE","MAO","MAR","AEG","GAS"],"RUSSIA":["MOS","BOT","BAR","UKR","SEV","STP","WAR","ARM","BUL","ANK","CON","FIN","BUD","RUM","BAL","GAL","SWE","SKA","TRI","SMY","SER","KIE","SYR","BLA","SIL"],"TURKEY":["ALB"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":["A TRI R ALB"],"RUSSIA":[],"TURKEY":[]},"results":{"A TRI":[]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5710850,"phase":"S1909R","message":"that was not very nice of you. after i supported your hold. wow, i did not really see that coming at all, i must admit."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5710898,"phase":"S1909R","message":"are you trying to help russia win? that seems like a foolish strategy, especially when you are in the thick of it and have a chance at being included in a draw."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5710924,"phase":"S1909R","message":"what happened to your support for me to take burgundy? i needed that..."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5710997,"phase":"S1909R","message":"are you happy now? russia is moving toward winning. england didn't even move against you at all - he wants peace and you insist on fighting so that you can lose and russia can win. a very odd strategy, even for a curmudgeon."}]},{"name":"F1909M","state":{"timestamp":1537459323173794,"zobrist_hash":"8199796031649720369","note":"","name":"F1909M","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A LON","A BEL","F BER","F EDI","F NWY"],"FRANCE":[],"GERMANY":["A BUR","A BRE","A MUN","A RUH","F DEN","F HOL"],"ITALY":["A TYR","F ENG","F GRE","F MAO","A MAR","A VEN","F AEG","A GAS","A ALB"],"RUSSIA":["A BUL","A BUD","A GAL","A SWE","A STP","F SKA","A TRI","F SMY","A SER","F KIE","A SYR","F BLA","A SIL"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP","BER","BEL"],"FRANCE":[],"GERMANY":["MUN","HOL","PAR","DEN","BRE","KIE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","SWE","ANK","BUD","CON","SMY"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH","VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","NTH","BEL","BER","NWY"],"FRANCE":[],"GERMANY":["HEL","LVN","PAR","PIC","BUR","BRE","MUN","RUH","DEN","HOL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","EAS","ION","ENG","PIE","SPA","GRE","MAO","MAR","AEG","GAS","ALB"],"RUSSIA":["MOS","BOT","BAR","UKR","SEV","STP","WAR","ARM","BUL","ANK","CON","FIN","BUD","RUM","BAL","GAL","SWE","SKA","TRI","SMY","SER","KIE","SYR","BLA","SIL"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE - BUD"],"ENGLAND":["A LON - WAL","A BEL - HOL","F BER S F KIE","F EDI - NTH","F NWY S F EDI - NTH"],"FRANCE":[],"GERMANY":["A BUR - BEL","A BRE - PAR","A MUN S F DEN - KIE","A RUH S A BUR - BEL","F DEN - KIE","F HOL S F DEN - KIE"],"ITALY":["A TYR - TRI","F ENG S A GAS - BRE","A MAR - BUR","F MAO - GAS","F GRE - BUL\/SC","A VEN S A TYR - TRI","A GAS - BRE","F AEG - BUL\/SC","A ALB - SER"],"RUSSIA":["A BUL S F BLA - CON","A BUD S A TRI","A GAL - BOH","A SWE S F SKA - DEN","A STP - FIN","A TRI S A MUN - TYR","A SER - GRE","F SMY - AEG","A SYR - ARM","F BLA - CON","A SIL S A GAL - BOH","F SKA - DEN","F KIE S F SKA - DEN"],"TURKEY":[]},"results":{"A VIE":["bounce"],"A LON":[],"A BEL":["bounce","dislodged"],"F BER":[],"F EDI":[],"F NWY":[],"A BUR":[],"A BRE":[],"A MUN":[],"A RUH":[],"F DEN":["bounce","dislodged"],"F HOL":["cut"],"A TYR":[],"F ENG":[],"F GRE":["bounce"],"F MAO":[],"A MAR":[],"A VEN":[],"F AEG":["bounce"],"A GAS":[],"A ALB":["bounce"],"A BUL":["cut"],"A BUD":["cut"],"A GAL":[],"A SWE":[],"A STP":[],"F SKA":[],"A TRI":["void","dislodged"],"F SMY":["bounce"],"A SER":["bounce"],"F KIE":[],"A SYR":[],"F BLA":[],"A SIL":[]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":5711024,"phase":"F1909M","message":"wow both austria and germany lost their marbles. damn. what a shite game this is turning into."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5711178,"phase":"F1909M","message":"so what happens from here? you will keep attacking me?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5712099,"phase":"F1909M","message":"To be absolutely honest with you...I want Greece from you as well. After that....I want to secure the seas East of the Ionian. Once that is acheived, I have no other goals that conflict with your own. I think I can get the rest of what I need from the West.<br \/><br \/>BTW....did you notice the moves in Norway? First time I ever pulled off that type of maneuver."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5712375,"phase":"F1909M","message":"i noticed that move, but why? i don't see the advantage to it. what am i missing?<br \/><br \/>well, there's nothing i can do to stop you from winning with both austria and germany fighting you. that is extremely frustrating. i'll fight you the best i can in the south, but am glad to coordinate where possible in the north."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5712565,"phase":"F1909M","message":"i am extremely frustrated with this situation but i guess i've profited off of similar ones in the past, so such is life. i have always avoided playing with mapleleaf until this unavoidable moment - i know now that that was a good idea and i will be sure never to play with him again. flashman doesn't seem to be much better - i don't see any sense at all in his working with you, obviously you can wipe him out at vienna now."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5712628,"phase":"F1909M","message":"so now russia will take vienna most likely. any chance i can get you to hit budapest so i could take back trieste and once again try to protect your position and keep you alive?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5712868,"phase":"F1909M","message":"your position in norway is quite tenuous now. and belgium too. thoughts for this turn?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5713418,"phase":"F1909M","message":"I told Flashman I would try to get him a survive.<br \/><br \/>I wanted to give England NOR...but to ensure he didn't convoy LON there I moved SWE to NOR....but I supported NS (the unit I did want there)...from STP.<br \/><br \/>I think we can work in the North. I have no problem working to eliminate Germany from the game completely."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5713521,"phase":"F1909M","message":"ah yes, i thought that might have been it. well done, nicely executed. <br \/><br \/>i promised flashman a survive or participation in the draw... too bad he chose you. so you are really going to let him live? you kinda need that SC."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5713626,"phase":"F1909M","message":"alright, i am desperate to not let russia win this game. you can take belgium this turn (even without my help) and probably get kiel back, while maybe russia will take out berlin. i will take brest, but from there we can reoup and hold the line against russia. i have been honest with you about my intentions through this process, and i am being honest now as well. if we can hold russia at 17 or less, i don't care how we do it."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":5713659,"phase":"F1909M","message":"I did have the support in, Germany cut it by moving into Belgium"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5713668,"phase":"F1909M","message":"if we can hold russia at seventeen, i'll be sure you are included in the draw. we're very close to being a draw if you and germany help stop the russians. let me know."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5713729,"phase":"F1909M","message":"no you didn't.<br \/><br \/>look at the orders history. germany can't cut the support to burgundy with burgundy, so if you had ordered it, i would have taken burgundy.<br \/><br \/> * The fleet at Berlin hold.<br \/> * The army at London hold.<br \/> * The army at Belgium hold.<br \/> * The fleet at North Sea move to Norway.<br \/> * The fleet at Edinburgh move to North Sea. (fail)"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5713762,"phase":"F1909M","message":"you will lose belgium this turn for sure. possibly berlin, and possibly norway, depending on what game exactly russia is playing."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5713790,"phase":"F1909M","message":"so are you just going to take norway back from england? and what about berlin, are you letting him keep that too?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5713814,"phase":"F1909M","message":"we can have germany down to one or two dots at the end of this turn - that would be nice."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5713939,"phase":"F1909M","message":"if we can get germany and austria to stop messing around after this turn, we can possibly hold russia at 16 or 17."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5714637,"phase":"F1909M","message":"Too many questions. Don't pester me....I've been mostly dead all day! (Princess Bride)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5714888,"phase":"F1909M","message":"lol"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5730732,"phase":"F1909M","message":"I need specifics. I helped Russia to try to force your hand."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5743390,"phase":"F1909M","message":"i told you i would support hold, and you can plainly see that i did exactly that. i was hoping germany would move against russia, but clearly he did not - so i couldn't give specifics beyond that turn.<br \/>if you help stop russia from winning, i'll be sure that you are included in the draw. your help is vital, and you will be crucial to the draw line, so you don't need to be afraid of being eliminated. or you can keep helping russia and get eliminated. <br \/><br \/>tell me what you want and i'll do my best to get it for you. but you're in a very vulnerable spot now - your support for russia hurt you more than it hurt me."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5744727,"phase":"F1909M","message":"I can offer to support Trieste (for Russia) to stop him attacking me. I can actually support you into Trieste instead, giving me two friendly neighbours. The problem though is Boh. If Russia takes that, I am somewhat doomed."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5778557,"phase":"F1909M","message":"yes that's why i had hoped germany would move there =(<br \/><br \/>offering support to russia in trieste seems wise, and supporting me in instead seems even wiser ;) i'll attack from tyr i suppose. let me know if you have other thoughts on the best way to stop purple from owning the board."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5779881,"phase":"F1909M","message":"Okay. But I think the above is the best combination."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":5779916,"phase":"F1909M","message":"I will support you in Trieste."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5780138,"phase":"F1909M","message":"i agree, let's do it. although maybe it would be more sure to succeed if you used vienna to cut support at bud?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5780225,"phase":"F1909M","message":"Okay, I will do that. He could hit Vienna from Galicia and break support. By hitting Bud, he definitely loses a support. I will change the orders."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5780249,"phase":"F1909M","message":"Done!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5780358,"phase":"F1909M","message":"super =)<br \/><br \/>any help you can offer in convincing germany to stop fighting me and england and help against russia would be welcome..."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5780471,"phase":"F1909M","message":"I will try, but I cannot signal to Germany that I am helping you... Not until after this turn."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":5780507,"phase":"F1909M","message":"Do you have any hope of getting a draw from this one?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5780537,"phase":"F1909M","message":"oh yeah, that's true. you could say that you are sticking with russia but that you want to be part of a draw, so encourage him to resist? or just wait until next turn, fair enough."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5780578,"phase":"F1909M","message":"I have asked him if he thinks he has any chance of a draw... I cannot suggest anything but am trying to get him thinking in that direction. He can surely count and see how close Russia is."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5780785,"phase":"F1909M","message":"alright, let's see what happens. he's told me he is going to throw the game to russia if i don't do everything he demands, so i am not hopeful. not a very pleasant character to play with, i have to admit."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":5787394,"phase":"F1909M","message":"I am putting pressure on Italy. I'll keep you posted. I may need your help in negotiations.<br \/>;0)"},{"sender":"GERMANY","recipient":"ITALY","time_sent":5787489,"phase":"F1909M","message":"If you attempt Brest, then it's all over."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5787530,"phase":"F1909M","message":"so what will you do if i don't take brest? will you actually fight russia instead of messing around with england?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":5787611,"phase":"F1909M","message":"your army at brest is useless against russia anyway, and i'm going to be forced to disband b\/c of losing trieste and\/or greece. but i'm willing to take that, if it means you will actually grow a backbone and stop being a douchebag wimp, by fighting russia."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5788963,"phase":"F1909M","message":"seriously, why is it that you want russia to win so badly? if you had actually defended against him this turn instead of continuing your attack on england (who didn't keep attacking you, i will note) he would have a much harder time getting a solo. instead, you're losing your home SCs, and look like a fool for letting another player win in a tournament where the only points go to people who get solo wins. if you don't work against russia with us from here on out, you are definitely one of the most pathetic players i have ever encountered."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5797052,"phase":"F1909M","message":"Grow up."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":5797089,"phase":"F1909M","message":"Italy has become childish and abusive. How can I help you?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":5797276,"phase":"F1909M","message":"you can't even be bothered to respond? you really want russia to win this game?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":5797386,"phase":"F1909M","message":"To expand, I have attempted to use my middle position to negotiate with Italy. She has ignored every request of mine while steadily encroaching upon my territory, and enabling England, my enemy this game. She has now become abusive, as if I would worry about her assessment of me(?). To punish her for her impudence and poor sportmanship, I will help you to victory, if you would like."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5797464,"phase":"F1909M","message":"I don't lower myself to respond to childish personal attacks. It demeans us both."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5797524,"phase":"F1909M","message":"ok, so respond to the question about your plans."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5797630,"phase":"F1909M","message":"You cannot call yourself an ally, while encroaching upon my territory, and enabling an enemy.<br \/><br \/>As regards England, I do not ASK you whom my enemies are, I TELL you."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5797791,"phase":"F1909M","message":"i have repeatedly asked you to work together and you have refused. i am trying to stop the russian from soloing - you are not. hence, i move to take your SCs since you are just handing them to the potential winner rather than putting up a fight.<br \/><br \/>it doesn't matter if you get berling and belgium back from england, if russia wins the game, does it? <br \/><br \/>your definition of enemy is shortsighted in light of the imminent victory by russia. you are not behaving in any way that makes sense to me, so i am at a loss and move to eliminate the random variable of a german that would rather right another minor power than stop the major power from getting the solo win, in the tournament where the only way to score points is a solo win. <br \/><br \/>it sure is an interesting strategy, i'll give you that."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5797817,"phase":"F1909M","message":"and i'm not asking you who your enemies are, I am telling YOU who they are, since you don't seem to grasp it."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":5801800,"phase":"F1909M","message":"I would certainly welcome any help that will give me a solo victory. If you offer assistance without conditions, then I will accept it."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5815025,"phase":"F1909M","message":"He told me he was putting pressure on you."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5822364,"phase":"F1909M","message":"pressure on me to do what? to attack england? that's the stupidest idea ever."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5841926,"phase":"F1909M","message":"Could you support hold me in Kiel?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5845488,"phase":"F1909M","message":"He didn't say. maybe he wants to try for a draw but doesn't trust you... Well, given the position, he is stupid not to trust you: he really has no choice but to ally against Russia. Does he think you and Russia will play for a two-way? That is the most dangerous game in town. Three-way, yes, but two-way - no."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5867688,"phase":"F1909M","message":"Sure thing."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5867743,"phase":"F1909M","message":"Nothing would probably come of it but could you support Belgium to Holland?"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":5868372,"phase":"F1909M","message":"Thanks. He has been attacking Belgium. Even if it succeeded you would probably break even. I have Kiel currently supporting myself into Denmark. I think the main goal here is to eliminate Mapleleaf....so I think we are on the right path. I think we easily get you Holland next year. Is that OK?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":5869014,"phase":"F1909M","message":"Grasp diplomacy, and then you'll really have something."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":5869121,"phase":"F1909M","message":"Certainly. I am stating that you will have nothing to lose by allowing me to fight Italy and England while you shore up your position."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5869149,"phase":"F1909M","message":"England signifies nothing."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":5869557,"phase":"F1909M","message":"Alright..but fair warning...I am taking Denmark. That should be the last SC I need from you."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":5873040,"phase":"F1909M","message":"Fair enough."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5889557,"phase":"F1909M","message":"i want to draw, i want to stop russia from soloing, i want to work with him. he refuses to help. he wants to fight england, not russia, which makes no sense to me. oh well."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5889622,"phase":"F1909M","message":"congratulations on losing the game. like i said, if you want to not be a loser, let me know and we can work together."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5889895,"phase":"F1909M","message":"OK this is weird, i haven't heard anything from you this entire turn. do i need to abandon you, my stalwart ally? non-communication is a critical problem at this juncture."}]},{"name":"F1909R","state":{"timestamp":1537459323176634,"zobrist_hash":"7650121244977030035","note":"","name":"F1909R","units":{"AUSTRIA":["A VIE"],"ENGLAND":["F BER","F NWY","A WAL","F NTH","*A BEL"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F HOL","A BEL","A PAR","*F DEN"],"ITALY":["F ENG","F GRE","A VEN","F AEG","A ALB","A TRI","F GAS","A BUR","A BRE"],"RUSSIA":["A BUL","A BUD","A SWE","F SMY","A SER","F KIE","A SIL","A BOH","A FIN","F DEN","A ARM","F CON"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP","BER","BEL"],"FRANCE":[],"GERMANY":["MUN","HOL","PAR","DEN","BRE","KIE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","NWY","BUL","SER","SWE","ANK","BUD","CON","SMY"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","BER","NWY","WAL","NTH"],"FRANCE":[],"GERMANY":["HEL","LVN","PIC","MUN","RUH","HOL","BEL","PAR"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","EAS","ION","ENG","PIE","SPA","GRE","MAO","MAR","AEG","ALB","TRI","GAS","BUR","BRE"],"RUSSIA":["MOS","BOT","BAR","UKR","SEV","STP","WAR","BUL","ANK","BUD","RUM","BAL","GAL","SWE","SKA","SMY","SER","KIE","SYR","BLA","SIL","BOH","FIN","DEN","ARM","CON"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["A BEL D"],"FRANCE":[],"GERMANY":["F DEN R HEL"],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A BEL":["disband"],"F DEN":[],"A TRI":["disband"]},"messages":[{"sender":"ITALY","recipient":"ENGLAND","time_sent":5898965,"phase":"F1909R","message":"hm, so i guess you didn't know what i was going to do? i am sticking with you, don't worry. you're a much better ally than england."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5899083,"phase":"F1909R","message":"well, we're in a precarious position now. i probably should have moved from venice. oh well. the nice thing is that if you get displaced maybe you can mess some things up in russia =)<br \/><br \/>i am thinking to build a fleet, but would an army make more sense do you think? <br \/><br \/>i'll keep trying to get germany on our side, but he is intractable. your continued help would be appreciated."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5899135,"phase":"F1909R","message":"that turn did not go well for you - perhaps you're willing to rethink your strategy and work with me to stop russia?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5899194,"phase":"F1909R","message":"alright, so what next? i don't suppose you would support me into munich? ;)<br \/><br \/>mapleleaf is certainly something else. my goodness."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":5911041,"phase":"F1909R","message":"Sorry...I kinda like the buffer between us in Munich. I am afraid it would put our armies in the north in too close proximity. ;)"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":5949050,"phase":"F1909R","message":"Yeah, I figured it wouldn't make it anyways."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":5955094,"phase":"F1909R","message":"I can't say I'm surprised, but it actually is a good thing for me, making it easier to know where my 18 th center will come from."},{"sender":"GERMANY","recipient":"ITALY","time_sent":5957816,"phase":"F1909R","message":"Grasp diplomacy, and then you'll really have something"}]},{"name":"W1909A","state":{"timestamp":1537459323178635,"zobrist_hash":"5992130478827045948","note":"","name":"W1909A","units":{"AUSTRIA":["A VIE"],"ENGLAND":["F BER","F NWY","A WAL","F NTH"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F HOL","A BEL","A PAR","F HEL"],"ITALY":["F ENG","F GRE","A VEN","F AEG","A ALB","A TRI","F GAS","A BUR","A BRE"],"RUSSIA":["A BUL","A BUD","A SWE","F SMY","A SER","F KIE","A SIL","A BOH","A FIN","F DEN","A ARM","F CON"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP","BER","NWY"],"FRANCE":[],"GERMANY":["MUN","HOL","PAR","BEL"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","BUL","SER","SWE","ANK","BUD","CON","SMY","DEN","KIE"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","BER","NWY","WAL","NTH"],"FRANCE":[],"GERMANY":["LVN","PIC","MUN","RUH","HOL","BEL","PAR","HEL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","EAS","ION","ENG","PIE","SPA","GRE","MAO","MAR","AEG","ALB","TRI","GAS","BUR","BRE"],"RUSSIA":["MOS","BOT","BAR","UKR","SEV","STP","WAR","BUL","ANK","BUD","RUM","BAL","GAL","SWE","SKA","SMY","SER","KIE","SYR","BLA","SIL","BOH","FIN","DEN","ARM","CON"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":1,"homes":["EDI","LON","LVP"]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":-2,"homes":[]},"ITALY":{"count":1,"homes":["NAP","ROM"]},"RUSSIA":{"count":2,"homes":["MOS","SEV","STP","WAR"]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["A LON B"],"FRANCE":[],"GERMANY":["A PAR D","F HEL D"],"ITALY":["F NAP B"],"RUSSIA":["F STP\/NC B","A WAR B"],"TURKEY":[]},"results":{"A LON":[""],"A PAR":[""],"F HEL":[""],"F NAP":[""],"F STP\/NC":[""],"A WAR":[""]},"messages":[{"sender":"RUSSIA","recipient":"GERMANY","time_sent":5959691,"phase":"W1909A","message":"How about disbanding Paris...I will assist HEL to the NS. Also would you be willing to consider moving MUN to TRL? If so, I would be willing to support you into Italian centers. I want to finish off Austria."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":5961232,"phase":"W1909A","message":"Desperate times here... I am duty bound to try to prevent a solo."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":5996628,"phase":"W1909A","message":"what the hell is going on with you? total silence? this is how you treat me for sticking up for you against germany? if i don't hear anything from you by the end of the phase, i am siding with germany against you - i can't work with someone who doesn't talk to me..."},{"sender":"ITALY","recipient":"GERMANY","time_sent":5996679,"phase":"W1909A","message":"Grasp diplomacy, then you'll lose less often.<br \/><br \/>regardless, i seem to have reached an impasse with england. i imagine there is no point in asking, but if i were to help you against england, would you work with me against russia?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":5996780,"phase":"W1909A","message":"understandable...."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6040623,"phase":"W1909A","message":"good lord, everyone has gone silent on me. this is a difficult way to play diplomacy."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6046230,"phase":"W1909A","message":"I'm here. Just been busy earning money."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6062014,"phase":"W1909A","message":"Of course I'll work with you against Russia under those conditions. That's what I've been pushing for. What do you suggest in terms of our cooperation?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":6062148,"phase":"W1909A","message":"What is the specific plan of action for replacing Mun? ....and why Paris?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":6062414,"phase":"W1909A","message":"i'm open to ideas."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6062516,"phase":"W1909A","message":"obviously keeping munich in your hands is a high priority. bu i also need to keep russia out of tyrolia. you should probably disband paris, and either hel or hel."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6062566,"phase":"W1909A","message":"you could move mun-tyr and i can support ruh-mun. that leaves hol quite exposed though."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":6065988,"phase":"W1909A","message":"Paris was only a suggestion, since it seems indefensible anyway. I was thinking we leave MUN open in the spring, to catch Italy off guard and bounce in the fall to keep an open territory for you. We get you VIE in the fall and move VIE to TRI the next year, with me following behind you into VIE."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6127344,"phase":"W1909A","message":"i am concerned that i haven't heard back from you. but it doesn't really affect my build choice, i suppose."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6127378,"phase":"W1909A","message":"then again, if i knew what you were disbanding, it might. i am planning to build a fleet, but i could build an army if that is more helpful."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6130945,"phase":"W1909A","message":"england, why the silence? i don't get it."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6130969,"phase":"W1909A","message":"no problem. any opinion on what i should build?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6131000,"phase":"W1909A","message":"can't wait to see what is in store for this year..."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6131241,"phase":"W1909A","message":"Fleet and move West. At some stage you might need to go round England. Right now, Russia is army-bound up north."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6131411,"phase":"W1909A","message":"west really? you don't think i'll need the fleet to hold russia to anatolia?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6131777,"phase":"W1909A","message":"He has only two fleets there, so he cannot break out."}]},{"name":"S1910M","state":{"timestamp":1537459323190243,"zobrist_hash":"3862370628250177589","note":"","name":"S1910M","units":{"AUSTRIA":["A VIE"],"ENGLAND":["F BER","F NWY","A WAL","F NTH","A LON"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F HOL","A BEL"],"ITALY":["F ENG","F GRE","A VEN","F AEG","A ALB","A TRI","F GAS","A BUR","A BRE","F NAP"],"RUSSIA":["A BUL","A BUD","A SWE","F SMY","A SER","F KIE","A SIL","A BOH","A FIN","F DEN","A ARM","F CON","F STP\/NC","A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP","BER","NWY"],"FRANCE":[],"GERMANY":["MUN","HOL","PAR","BEL"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","BUL","SER","SWE","ANK","BUD","CON","SMY","DEN","KIE"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","BER","NWY","WAL","NTH"],"FRANCE":[],"GERMANY":["LVN","PIC","MUN","RUH","HOL","BEL","PAR","HEL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","EAS","ION","ENG","PIE","SPA","GRE","MAO","MAR","AEG","ALB","TRI","GAS","BUR","BRE"],"RUSSIA":["MOS","BOT","BAR","UKR","SEV","STP","WAR","BUL","ANK","BUD","RUM","BAL","GAL","SWE","SKA","SMY","SER","KIE","SYR","BLA","SIL","BOH","FIN","DEN","ARM","CON"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE S A MUN - BOH"],"ENGLAND":["F BER H","F NWY H","F NTH H","A WAL H","A LON H"],"FRANCE":[],"GERMANY":["A MUN H","A RUH S A MUN","F HOL - NTH","A BEL - HOL"],"ITALY":["F ENG S F HOL - NTH","F GRE S F AEG - BUL","A VEN S A TRI","F AEG - BUL\/SC","A ALB - SER","A BUR S A MUN","A BRE - GAS","F GAS - MAO","A TRI S A ALB - SER","F NAP - ION"],"RUSSIA":["A BUL S A SER - GRE","A BUD - RUM","A SWE S F STP\/NC - NWY","A SER - GRE","F SMY S F CON - AEG","A SIL S A WAR - GAL","F KIE H","A ARM - SEV","A FIN S F STP\/NC - NWY","F DEN - NTH","F CON - AEG","A BOH S A BUD - VIE","A WAR - GAL","F STP\/NC - NWY"],"TURKEY":[]},"results":{"A VIE":["void"],"F BER":[],"F NWY":["dislodged"],"A WAL":[],"F NTH":["dislodged"],"A LON":[],"A MUN":[],"A RUH":[],"F HOL":[],"A BEL":[],"F ENG":[],"F GRE":["cut"],"A VEN":[],"F AEG":["bounce","dislodged"],"A ALB":[],"A TRI":[],"F GAS":[],"A BUR":[],"A BRE":[],"F NAP":[],"A BUL":["cut"],"A BUD":[],"A SWE":[],"F SMY":[],"A SER":["bounce","dislodged"],"F KIE":[],"A SIL":[],"A BOH":["void"],"A FIN":[],"F DEN":["bounce"],"A ARM":[],"F CON":[],"F STP\/NC":[],"A WAR":[]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6131972,"phase":"S1910M","message":"heh, i suppose you are right. i expected him to build a third, but i guess not."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6132007,"phase":"S1910M","message":"thoughts on the best way forward here?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6132023,"phase":"S1910M","message":"england has totally stopped communicating with me, it's very strange."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6132682,"phase":"S1910M","message":"I know...it's SO exciting!"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6133208,"phase":"S1910M","message":"lol<br \/><br \/>i expected a fleet build in the south. but the north does make sense too."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6133232,"phase":"S1910M","message":"i don't really understand why you build an army when a fleet at edi would be so helpful to stop russia. but i guess you have given up on this game?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6133650,"phase":"S1910M","message":"What doesn't make sense is.....my stupid Armenian army on holiday :("},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6133817,"phase":"S1910M","message":"haha - they've been touring the holy sites in the middle east cut them some slack ;D"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6133835,"phase":"S1910M","message":"so did you convince england to build that army? if so, job well done."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6134046,"phase":"S1910M","message":"Not directly....but we had discussed a land attack."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6134092,"phase":"S1910M","message":"oh wow. so he's not even bothering to try to stop you from winning? what a mess."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6134149,"phase":"S1910M","message":"But that's probably the last bridge burnt with England....unless we play another 10 years, then maybe by that time, I can build a new bridge to burn down."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6134346,"phase":"S1910M","message":"yeah i suppose so. <br \/><br \/>you need 4 SCs. nwy is given, vienna it seems like as well. greece is probably just a matter of time. the last one will most likely be berlin i suppose. seems like we'll be done here in a couple turns. congrats on your win - manipulating england deserves a tip of the hat, but you get no credit for germany's ridiculous behavior... ;D"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6134373,"phase":"S1910M","message":"oh i see, russia talked you into it. well, good on him. too bad for all of us."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6134417,"phase":"S1910M","message":"You are the only one now trying to stop me...well, Austria too...but he just killed himself last turn. So why aren't you on the bandwagon....??? You should just give up and concentrate on the other TMG games going on. You really don't need this frustration here."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6134531,"phase":"S1910M","message":"ahaha true true. i guess i'm just an individualist like that... lol"},{"sender":"GERMANY","recipient":"ITALY","time_sent":6146779,"phase":"S1910M","message":"You need to lock down NAt. Also, I'll probably need support from Bur in Mun this autumn. We have time to coordinate and force a draw."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6147554,"phase":"S1910M","message":"ok, you got it. is there a way that brest can be helpful?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6164200,"phase":"S1910M","message":"England seems to have set up a fortress. He has no presence in the Western waters. That could have been agreed with Russia, who now does have a northern fleet to play with."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6169501,"phase":"S1910M","message":"yup, well played by russia, idiotic of england. what a pair, him and germany. at least germany is willing to work with me now, though it's a bit late i fear."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6176817,"phase":"S1910M","message":"Can you get Germany to help me with Vienna? I may sound selfish (in Diplomacy?) but we all need to keep Russia out."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":6182844,"phase":"S1910M","message":"I assume you will use all 3 of your units to support Belgium? I would ask you to move to Tyrolia. You have the option in the fall to either return to Munich or attempt Trieste\/Vienna"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6213507,"phase":"S1910M","message":"what ever you want to see happen, i'll suggest to the german - you should too, tho. mun-boh, is that what you have in mind? or something else?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":6214026,"phase":"S1910M","message":"would you be willing to use munich to hit bohemia, and i'll support ruh-mun? it would be good to keep vienna out of russian hands, if possible."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6214049,"phase":"S1910M","message":"this is not like you, sayjo, to be so quiet. are you ok?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6216935,"phase":"S1910M","message":"Mun to Boh would do fine."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6217008,"phase":"S1910M","message":"ok, i've suggested that to him. let's see."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6217079,"phase":"S1910M","message":"Thanks."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6217082,"phase":"S1910M","message":"i can see how my naples fleet would be helpful in the north, bu i also wonder if it might be able to help push russia back in the south. what's your thought?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6217164,"phase":"S1910M","message":"It looks like he will try to push you out of the Aegean, so I think you have a real need to stiffen that defensive line. You need England to block passage through the Norwegian Sea."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6217234,"phase":"S1910M","message":"but england is not responding to me at all. for two turns now..."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6217391,"phase":"S1910M","message":"can we please work together to hold back russia? please?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":6217716,"phase":"S1910M","message":"re:Brest. Convoy it to England or swing it back through Italy. Will you please support Hol-Nth? I doubt it will work, but you never know."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6217809,"phase":"S1910M","message":"Mun will get dislodged if it bounces with Boh in Tyr."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6217921,"phase":"S1910M","message":"I know, you mentioned that. I will try..."},{"sender":"AUSTRIA","recipient":"ENGLAND","time_sent":6217970,"phase":"S1910M","message":"Hi, any chance you can stop Russia entering the Norwegian Sea? If he does, he outflanks you and even if he goes past you, he gets out and wins the game."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6218007,"phase":"S1910M","message":"not if Bur supports Ruh-Mun. and the suggestion was for mun-boh, not mun-tyr. what do you think?<br \/><br \/>yes i can support hol-nth, and i'll send bre toward italy"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6218023,"phase":"S1910M","message":"thanks,."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6218084,"phase":"S1910M","message":"It is in my interest to do so!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6218189,"phase":"S1910M","message":"indeed..."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6218227,"phase":"S1910M","message":"also, what would it take for you to work with england rather than against him?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":6218242,"phase":"S1910M","message":"although of course he is being completely useless right now."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6218258,"phase":"S1910M","message":"once russia hits him hard this turn, i imagine his tune will change a bit, though."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6303248,"phase":"S1910M","message":"the italian envoy kindly requests an audience with the king..."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6303283,"phase":"S1910M","message":"ok, are we all set on moves this turn? can you send mun-boh and ruh-mun and i'll supprot ruh-mun?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":6303468,"phase":"S1910M","message":"are you sure moving to nth is a good idea? won't that just leave hol open for russia?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6303563,"phase":"S1910M","message":"please? pretty please?"}]},{"name":"S1910R","state":{"timestamp":1537459323193009,"zobrist_hash":"3035563051176190800","note":"","name":"S1910R","units":{"AUSTRIA":["A VIE"],"ENGLAND":["F BER","A WAL","A LON","*F NWY","*F NTH"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F NTH","A HOL"],"ITALY":["F ENG","F GRE","A VEN","A TRI","A BUR","A SER","F MAO","A GAS","F ION","*F AEG"],"RUSSIA":["A BUL","A SWE","F SMY","F KIE","A SIL","A BOH","A FIN","F DEN","A RUM","A SEV","F AEG","F NWY","A GAL","*A SER"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP","BER","NWY"],"FRANCE":[],"GERMANY":["MUN","HOL","PAR","BEL"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","BUL","SER","SWE","ANK","BUD","CON","SMY","DEN","KIE"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["LVP","YOR","CLY","LON","NWG","EDI","BER","WAL"],"FRANCE":[],"GERMANY":["LVN","PIC","MUN","RUH","BEL","PAR","HEL","NTH","HOL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","EAS","ENG","PIE","SPA","GRE","MAR","ALB","TRI","BUR","BRE","SER","MAO","GAS","ION"],"RUSSIA":["MOS","BOT","BAR","UKR","STP","WAR","BUL","ANK","BUD","BAL","SWE","SKA","SMY","KIE","SYR","BLA","SIL","BOH","FIN","DEN","ARM","CON","RUM","SEV","AEG","NWY","GAL"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NTH R EDI","F NWY R NWG"],"FRANCE":[],"GERMANY":[],"ITALY":["F AEG R EAS"],"RUSSIA":["A SER R BUD"],"TURKEY":[]},"results":{"F NWY":[],"F NTH":[],"F AEG":[],"A SER":[]},"messages":[{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":6313524,"phase":"S1910R","message":"Damn...I thought Italy would be helping you against me. I should have known she sold you out to the German."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":6314354,"phase":"S1910R","message":"damn, england. seriously? NMR at this juncture in the game?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6314616,"phase":"S1910R","message":"sorry, i didn't hear back from germany so i decided to support his hold. i'll try again to get him to tap bohemia."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6332393,"phase":"S1910R","message":"Sorry, bent, I'm really busy at work right now. I don't mean to be so uncommunicative, and I know how difficult it makes things."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6332500,"phase":"S1910R","message":"We have time to plan the autumn orders."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6335246,"phase":"S1910R","message":"i will only be online sparodically the next few days b\/c i'll be traveling. so we'll have the make the most of each message."}]},{"name":"F1910M","state":{"timestamp":1537459323206142,"zobrist_hash":"983592890230508802","note":"","name":"F1910M","units":{"AUSTRIA":["A VIE"],"ENGLAND":["F BER","A WAL","A LON","F EDI","F NWG"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F NTH","A HOL"],"ITALY":["F ENG","F GRE","A VEN","A TRI","A BUR","A SER","F MAO","A GAS","F ION","F EAS"],"RUSSIA":["A BUL","A SWE","F SMY","F KIE","A SIL","A BOH","A FIN","F DEN","A RUM","A SEV","F AEG","F NWY","A GAL","A BUD"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP","BER","NWY"],"FRANCE":[],"GERMANY":["MUN","HOL","PAR","BEL"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","BUL","SER","SWE","ANK","BUD","CON","SMY","DEN","KIE"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["LVP","YOR","CLY","LON","BER","WAL","EDI","NWG"],"FRANCE":[],"GERMANY":["LVN","PIC","MUN","RUH","BEL","PAR","HEL","NTH","HOL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","TYR","VEN","ENG","PIE","SPA","GRE","MAR","ALB","TRI","BUR","BRE","SER","MAO","GAS","ION","EAS"],"RUSSIA":["MOS","BOT","BAR","UKR","STP","WAR","BUL","ANK","BAL","SWE","SKA","SMY","KIE","SYR","BLA","SIL","BOH","FIN","DEN","ARM","CON","RUM","SEV","AEG","NWY","GAL","BUD"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE - BUD"],"ENGLAND":["F BER H","A WAL S A LON","A LON H","F NWG - NTH","F EDI S F NWG - NTH"],"FRANCE":[],"GERMANY":["A MUN - KIE","A RUH S A MUN - KIE","F NTH - DEN","A HOL S A MUN - KIE"],"ITALY":["F ENG - LON","F GRE S A SER - BUL","A VEN - TYR","A BUR - MUN","A TRI S A VIE - BUD","F ION S F EAS - AEG","A SER - BUL","A GAS - MAR","F MAO - NAO","F EAS - AEG"],"RUSSIA":["A BUL - SER","A SWE S F KIE - DEN","F SMY H","A SIL S F KIE - BER","F KIE - BER","A FIN S F NWY","F DEN - NTH","A BOH - VIE","F AEG - BUL\/SC","A SEV - ARM","A GAL S A BOH - VIE","A RUM S A BUL - SER","F NWY S F NTH","A BUD - TRI"],"TURKEY":[]},"results":{"A VIE":[],"F BER":["dislodged"],"A WAL":[],"A LON":[],"F EDI":[],"F NWG":[],"A MUN":[],"A RUH":[],"F NTH":["bounce","dislodged"],"A HOL":[],"F ENG":["bounce"],"F GRE":[],"A VEN":[],"A TRI":[],"A BUR":[],"A SER":["bounce"],"F MAO":[],"A GAS":[],"F ION":[],"F EAS":[],"A BUL":["bounce"],"A SWE":["void"],"F SMY":[],"F KIE":[],"A SIL":[],"A BOH":[],"A FIN":[],"F DEN":["bounce"],"A RUM":[],"A SEV":[],"F AEG":["bounce","dislodged"],"F NWY":["void"],"A GAL":[],"A BUD":["bounce","dislodged"]},"messages":[{"sender":"ITALY","recipient":"GERMANY","time_sent":6562972,"phase":"F1910M","message":"alright, what's next?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6562995,"phase":"F1910M","message":"what do you want to do from here?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6563015,"phase":"F1910M","message":"still the silent treatment?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6569386,"phase":"F1910M","message":"Survive! Things are not going too well."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6572206,"phase":"F1910M","message":"My two cents? OK.<br \/><br \/>you : Ion-Aeg, Eas S Ion-Aeg, Gre S Ion-Aeg, Tri S H Ser, Ser S Vie-Bud, Ven-Tyr, Bur-Mun, Gas-Mar, Eng-Iri, and Mid-NAt. <br \/><br \/>me : Mun-Kie, Ruh S Mun-Kie, Hol S Mun-Kie, Nth-Den.<br \/><br \/>I'm hoping that you and Rus will bounce in Mun. It's the only way I'll get a build.<br \/><br \/>What do you think?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6585382,"phase":"F1910M","message":"germany suggests me supporting you into budapest. what do you think?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":6585425,"phase":"F1910M","message":"seems reasonable to me, but let's touch base again. i'll keep it as a provisional order, but i'll let you know if we need to change it, or to confirm it."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":6605672,"phase":"F1910M","message":"I am withdrawing from this tournament. My decision was made much earlier and is the result of a dispute in the Premier League with TGM. I had hoped to play out the three games I am in here already and at least give my successor a decent position in the third game to take over. Obviously, my two Austrias will provide little of value as I am down to one and two units respectively.<br \/><br \/>I have though just been multi-stabbed in the third game so the hope of a decent hand-over has gone.<br \/><br \/>I am in fact withdrawing from all competitions on this site run under the current rules."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":6609235,"phase":"F1910M","message":"does this mean you aren't entering moves in this game?"},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":6620317,"phase":"F1910M","message":"No, I will enter moves. I have no intention to spoil anything. Indeed, playing in a spoilt game is something I really detest: it is what happened in our League Game."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":6621643,"phase":"F1910M","message":"In fact, just tell me what you want. I really can do no more than that."},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":6645026,"phase":"F1910M","message":"Ooo...me...me....pick me! Dibs! Right, now go ahead and support me to Trieste. Thanks."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6645109,"phase":"F1910M","message":"Okay, try that... I am ordering myself to move."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":6645179,"phase":"F1910M","message":"Okay, Budapest to Trieste... I want vodka in return, and some of that rather nice caviar."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6645206,"phase":"F1910M","message":"Ignore the Global: I am doing what you suggested and moving to Bud."},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":6645610,"phase":"F1910M","message":"I'll even throw in a dancing bear!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6649550,"phase":"F1910M","message":"haha ok, i hoped so =)"},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":6653939,"phase":"F1910M","message":"Women. They go down pretty well with my troops."}]},{"name":"F1910R","state":{"timestamp":1537459323208884,"zobrist_hash":"2706806971107751293","note":"","name":"F1910R","units":{"AUSTRIA":["A BUD"],"ENGLAND":["A WAL","A LON","F EDI","F NTH","*F BER"],"FRANCE":[],"GERMANY":["A RUH","A HOL","A KIE","*F NTH"],"ITALY":["F ENG","F GRE","A TRI","A SER","F ION","A TYR","A MUN","F NAO","A MAR","F AEG"],"RUSSIA":["A BUL","A SWE","F SMY","A SIL","A FIN","F DEN","A RUM","F NWY","A GAL","F BER","A VIE","A ARM","*F AEG"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP","BER","NWY"],"FRANCE":[],"GERMANY":["MUN","HOL","PAR","BEL"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","BUL","SER","SWE","ANK","BUD","CON","SMY","DEN","KIE"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD"],"ENGLAND":["LVP","YOR","CLY","LON","WAL","EDI","NWG","NTH"],"FRANCE":[],"GERMANY":["LVN","PIC","RUH","BEL","PAR","HEL","HOL","KIE"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","ALB","TRI","BUR","BRE","SER","MAO","GAS","ION","EAS","TYR","MUN","NAO","MAR","AEG"],"RUSSIA":["MOS","BOT","BAR","UKR","STP","WAR","BUL","ANK","BAL","SWE","SKA","SMY","SYR","BLA","SIL","BOH","FIN","DEN","CON","RUM","SEV","NWY","GAL","BER","VIE","ARM"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F BER D"],"FRANCE":[],"GERMANY":["F NTH R BEL"],"ITALY":[],"RUSSIA":["F AEG R CON"],"TURKEY":[]},"results":{"F BER":["disband"],"F NTH":[],"F AEG":[],"A BUD":["disband"]},"messages":[{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":6685710,"phase":"F1910R","message":"Ha, life left in the old Flash after all. I even got back home for a while."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6693963,"phase":"F1910R","message":"ack, i just noticed i took munich. sorry!<br \/><br \/>at least you made up for it. we are advancing nicely, i think, no?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6693975,"phase":"F1910R","message":"well done!"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6694019,"phase":"F1910R","message":"can we pleeeeease talk? i don't want to keep attacking you - i'd like your help moving against russia..."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6698488,"phase":"F1910R","message":"Yes, that did go well didn't it. So pleased to see you in Tyrolia this time as well."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6731337,"phase":"F1910R","message":"yes. we should be able to get you back into vienna if that is appealing, right? thoughts on builds? definitely an army at venice, and i am thinking maybe a fleet at naples? <br \/><br \/>england is completely refusing to cooperate (or even communicate) so i guess his fate is sealed."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6731525,"phase":"F1910R","message":"so... do you feel secure in the western med yet? perhaps we could find a way to wrap this game up? <br \/><br \/>also, have you been communicating with england? he's gone kinda wacky on me..."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6731764,"phase":"F1910R","message":"What do you mean wrap it up? What are you expecting as an outcome?<br \/><br \/>Maybe England doesn't appreciate too much you assisting Germany and attacking London. :)"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6731850,"phase":"F1910R","message":"I agree with the builds: you still need fleets to stop movement around into the MAO. As for England, have you played with him before?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6732094,"phase":"F1910R","message":"i only did that after he started to go wacky ;D"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6732113,"phase":"F1910R","message":"no, i haven't. have you?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6732142,"phase":"F1910R","message":"i anticipate a draw that includes me and you. who else, i'm not sure."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6732308,"phase":"F1910R","message":"How about the rest of the game? You have MOST of the other nations on your side. I have none."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6732460,"phase":"F1910R","message":"i have germany and 1 SC austria. you have england. you also have more SCs - fairly balanced i'd say. <br \/><br \/>the rest of the game is what i want to discuss with you. what would you like to see happen?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6733070,"phase":"F1910R","message":"You have position on England and it's a guessing game in the Balkan regions. I was unable to establish an eastern line so I think things are very much up in the air. Number of SC's won't matter if you have all the allies. I was kinda glad Mapleleaf was back on your side, but I kinda also needed him."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6733164,"phase":"F1910R","message":"how would you like to see things resolve themselves?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6733319,"phase":"F1910R","message":"I suppose I haven't given up on a win yet. But I am open to drawng some boundaries."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6733413,"phase":"F1910R","message":"ah ok. that's fine. and you may well get it with england's behavior. plus, as you say, the balkans are up in the air. <br \/><br \/>what do you have in mind regarding boundaries?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6736366,"phase":"F1910R","message":"I am at work... I will review this this evening."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6736623,"phase":"F1910R","message":"sounds good, i look forward to seeing what you have to say."},{"sender":"GERMANY","recipient":"ITALY","time_sent":6756570,"phase":"F1910R","message":"Better you than Russia. We can figure out a way for you to vacate Mun anyway, so it's just a visit."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6756689,"phase":"F1910R","message":"that works for me..."}]},{"name":"W1910A","state":{"timestamp":1537459323210693,"zobrist_hash":"3542296889684194424","note":"","name":"W1910A","units":{"AUSTRIA":["A BUD"],"ENGLAND":["A WAL","A LON","F EDI","F NTH"],"FRANCE":[],"GERMANY":["A RUH","A HOL","A KIE","F BEL"],"ITALY":["F ENG","F GRE","A TRI","A SER","F ION","A TYR","A MUN","F NAO","A MAR","F AEG"],"RUSSIA":["A BUL","A SWE","F SMY","A SIL","A FIN","F DEN","A RUM","F NWY","A GAL","F BER","A VIE","A ARM","F CON"],"TURKEY":[]},"centers":{"AUSTRIA":["BUD"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":[],"GERMANY":["HOL","PAR","BEL","KIE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","MUN","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","BUL","SWE","ANK","CON","SMY","DEN","VIE","BER","NWY"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD"],"ENGLAND":["LVP","YOR","CLY","LON","WAL","EDI","NWG","NTH"],"FRANCE":[],"GERMANY":["LVN","PIC","RUH","PAR","HEL","HOL","KIE","BEL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","ALB","TRI","BUR","BRE","SER","MAO","GAS","ION","EAS","TYR","MUN","NAO","MAR","AEG"],"RUSSIA":["MOS","BOT","BAR","UKR","STP","WAR","BUL","ANK","BAL","SWE","SKA","SMY","SYR","BLA","SIL","BOH","FIN","DEN","RUM","SEV","NWY","GAL","BER","VIE","ARM","CON"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":-1,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":2,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":1,"homes":["MOS","SEV","STP","WAR"]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI D"],"FRANCE":[],"GERMANY":[],"ITALY":["F NAP B","A VEN B"],"RUSSIA":["F SEV B"],"TURKEY":[]},"results":{"F EDI":[""],"F NAP":[""],"A VEN":[""],"F SEV":[""]},"messages":[{"sender":"ITALY","recipient":"GERMANY","time_sent":6756984,"phase":"W1910A","message":"army venice, fleet naples make sense?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":6757022,"phase":"W1910A","message":"what ever england disbands, it will be good news for us =)"},{"sender":"GERMANY","recipient":"ITALY","time_sent":6757241,"phase":"W1910A","message":"Yea, that's what I would build..."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":6757649,"phase":"W1910A","message":"Wow....you really had me fooled. I thought you were unable to work with Italy. I underestimated your willingness to allow her to advance. I realize that I didn't have a great plan....but I did have intentions on trying to keep what you had instead of exchanging it."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6778230,"phase":"W1910A","message":"I think maybe once but without checking I am not certain. It might just be that I recognise his name from a Forum thread in which we have both posted. I do not recall any specific discussion with him about a game."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":6781932,"phase":"W1910A","message":"Well after reviewing the layout, I don't have anything I am willing to offer. Serbia and Budapest are still contentious areas. Looking back, I should have left Greece alone and fortified the areas north. C'est la vie."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":6787846,"phase":"W1910A","message":"i think you are correct. if you do decide on anything, let me know."}]},{"name":"S1911M","state":{"timestamp":1537459323222437,"zobrist_hash":"2080347370268048614","note":"","name":"S1911M","units":{"AUSTRIA":["A BUD"],"ENGLAND":["A WAL","A LON","F NTH"],"FRANCE":[],"GERMANY":["A RUH","A HOL","A KIE","F BEL"],"ITALY":["F ENG","F GRE","A TRI","A SER","F ION","A TYR","A MUN","F NAO","A MAR","F AEG","F NAP","A VEN"],"RUSSIA":["A BUL","A SWE","F SMY","A SIL","A FIN","F DEN","A RUM","F NWY","A GAL","F BER","A VIE","A ARM","F CON","F SEV"],"TURKEY":[]},"centers":{"AUSTRIA":["BUD"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":[],"GERMANY":["HOL","PAR","BEL","KIE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","MUN","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","BUL","SWE","ANK","CON","SMY","DEN","VIE","BER","NWY"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD"],"ENGLAND":["LVP","YOR","CLY","LON","WAL","EDI","NWG","NTH"],"FRANCE":[],"GERMANY":["LVN","PIC","RUH","PAR","HEL","HOL","KIE","BEL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","ALB","TRI","BUR","BRE","SER","MAO","GAS","ION","EAS","TYR","MUN","NAO","MAR","AEG"],"RUSSIA":["MOS","BOT","BAR","UKR","STP","WAR","BUL","ANK","BAL","SWE","SKA","SMY","SYR","BLA","SIL","BOH","FIN","DEN","RUM","SEV","NWY","GAL","BER","VIE","ARM","CON"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD - VIE"],"ENGLAND":["A WAL - LVP","A LON - YOR","F NTH - LON"],"FRANCE":[],"GERMANY":["A RUH - MUN","A HOL S A KIE","A KIE S A RUH - MUN","F BEL S F ENG - NTH"],"ITALY":["F ENG H","F GRE S F AEG - BUL","A TRI S A BUD - VIE","F ION - EAS","A SER S F AEG - BUL","A TYR S A BUD - VIE","A MUN - BOH","A MAR - GAS","F NAO - NWG","F AEG - BUL\/SC","F NAP - ION","A VEN S A TRI"],"RUSSIA":["A BUL H","A SWE - DEN","F SMY - AEG","A SIL S F BER","A FIN - STP","F DEN - BAL","A GAL S A RUM - BUD","A RUM - BUD","F NWY S F NTH","A ARM - SEV","F BER S F DEN - BAL","A VIE S A RUM - BUD","F CON S A BUL","F SEV - BLA"],"TURKEY":[]},"results":{"A BUD":[],"A WAL":[],"A LON":[],"F NTH":[],"A RUH":[],"A HOL":[],"A KIE":[],"F BEL":["void"],"F ENG":[],"F GRE":[],"A TRI":[],"A SER":[],"F ION":[],"A TYR":[],"A MUN":[],"F NAO":[],"A MAR":[],"F AEG":[],"F NAP":[],"A VEN":[],"A BUL":["dislodged"],"A SWE":[],"F SMY":[],"A SIL":[],"A FIN":[],"F DEN":[],"A RUM":[],"F NWY":["void"],"A GAL":[],"F BER":[],"A VIE":["cut","dislodged"],"A ARM":[],"F CON":[],"F SEV":[]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6817274,"phase":"S1911M","message":"ok, plan for this turn? i am thinking maybe bud to vienna, but that might well not work."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6817306,"phase":"S1911M","message":"i miss talking with sayjo... he's a nice funny guy... and i don't understand his silence... it makes me feel sad...."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6820434,"phase":"S1911M","message":"alright, came plan for this turn?getting control of the nth seems pretty crucial to me, shall i suppose bel-nth?i know it might fail - i can also order nao-nwg to set up for next turn.<br \/><br \/>in the middle i can see a couple ways forward - do you have a preference?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6826249,"phase":"S1911M","message":"Bud to Vienna works with two supports if Bohemia is blocked so that he cannot retreat. A disband for him would be very nice and might get you Silesia or Boh for the Autumn."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6826369,"phase":"S1911M","message":"that sounds good to me =)"},{"sender":"GERMANY","recipient":"ITALY","time_sent":6827231,"phase":"S1911M","message":"I suggest...you - NAt-Nrg, Eng-Nth, Aeg-Bul(sc), Gre S Aeg-Bul, Ser S Aeg-Bul, Ion-Aeg, Tri S Bud, Ven S Tri, Tyr S Mun-Boh, Mun-Boh, Mar-Pie, Nap-Tyn.<br \/><br \/>me - Bel S Eng-Nth, Ruh-Kie, Hol S Ruh-Kie, Kie-Mun.<br \/><br \/>What do you think? Any risks that do not work out for us can easily be corrected in the autumn with the position that we get."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6827466,"phase":"S1911M","message":"this sounds good to me, but austria and i had talked about trying to get budapest into vienna, to force a disband for russia. what do you think of that?<br \/><br \/>and out of curiousity, why send EC-Nth and not Bel-Nth? Seems like it is more advantageous to have a fleet in EC to support Nth-Lon in autumn?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":6860168,"phase":"S1911M","message":"Things happened outside the game that caused for a sitter for awhile and most of the games turned into Gunboats."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6904137,"phase":"S1911M","message":"ah, that explains it. sorry to hear that. hope all is well with you.<br \/><br \/>so, where do you stand at this point? are you back for the long term, or is this just a quick pop in?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":6905809,"phase":"S1911M","message":"I would prefer to simply eliminate Austria. <br \/><br \/>\"Too Many Dicks on the Dance Floor\" <br \/><br \/>Regarding my fleet in Bel, I enjoy staying curled up in the fetal position."},{"sender":"ITALY","recipient":"GERMANY","time_sent":6907076,"phase":"S1911M","message":"agreed about austria. but you think his usefulness has expired?<br \/><br \/>as for your fetal position, i can appreciate it, but i think it makes more sense for you to take Nth and then London (or Edi). would you be game for making that change? otherwise, i'm on board with this set of moves.<br \/><br \/>i finally heard something from england - seems he had a sitter who stopped communicating in all his games. i may be able to get him to move against russia (and we can sweep him from behind. any thoughts on that?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":6938915,"phase":"S1911M","message":"I'm trying to stay but 2 weeks gone left me with chaos to come back to so it's hard to get as motivated as I was. Sitter couldn't handle so many games lol so some of my \"sure things\" became wtf happened >.<"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":6938949,"phase":"S1911M","message":"Things are not well but the past 2 days have been the best they've been in a year."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6999261,"phase":"S1911M","message":"I have ordered Bud to Vienna."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6999452,"phase":"S1911M","message":"super. i've got all the orders in - they're not what germany wants, fyi, so hopefully he won't throw a hissy fit."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":6999679,"phase":"S1911M","message":"He is in a desperate position."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":6999945,"phase":"S1911M","message":"indeed he is."},{"sender":"GERMANY","recipient":"ITALY","time_sent":7010383,"phase":"S1911M","message":"England and I were into it long before the sitter, I think. So, no thank you. England, living , is the most dangerous Great Power."},{"sender":"GERMANY","recipient":"ITALY","time_sent":7010635,"phase":"S1911M","message":"There is no guarantee that Nth and Eng would capture Lon this autumn anyway. I am conservative by nature. There is no hurry for us up north. We have control. You have army superiority, so I could build fleets to aid our cause. Plus, you have the fleet in Nap to come sailing over.<br \/><br \/>I must stay in Bel, and I will be happy to support your endeavours from there."},{"sender":"ITALY","recipient":"GERMANY","time_sent":7011121,"phase":"S1911M","message":"ok well i want to stay in EC, so i guess we'll just leave Nth to the english, or more likely the russians...<br \/><br \/>i am not sending Nap west just yet - i need to see how the southeast resolves itself. i don't feel that we have control, but i am glad for your confidence. <br \/><br \/>if you were to move bel-nth, i could move nao-cly,a nd support you into edi if it looked like lon was not going to work. but i see you are set in staying in belgium, so such is life."}]},{"name":"S1911R","state":{"timestamp":1537459323225171,"zobrist_hash":"4500644911867386573","note":"","name":"S1911R","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A LVP","A YOR","F LON"],"FRANCE":[],"GERMANY":["A HOL","A KIE","F BEL","A MUN"],"ITALY":["F ENG","F GRE","A TRI","A SER","A TYR","A VEN","F EAS","A BOH","F NWG","A GAS","F BUL\/SC","F ION"],"RUSSIA":["A SIL","F NWY","A GAL","F BER","F CON","A DEN","F AEG","A STP","F BAL","A BUD","A SEV","F BLA","*A BUL"],"TURKEY":[]},"centers":{"AUSTRIA":["BUD"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":[],"GERMANY":["HOL","PAR","BEL","KIE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","MUN","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","BUL","SWE","ANK","CON","SMY","DEN","VIE","BER","NWY"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","EDI","NTH","LVP","YOR","LON"],"FRANCE":[],"GERMANY":["LVN","PIC","RUH","PAR","HEL","HOL","KIE","BEL","MUN"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","ALB","TRI","BUR","BRE","SER","MAO","TYR","NAO","MAR","EAS","BOH","NWG","GAS","BUL","ION"],"RUSSIA":["MOS","BOT","BAR","UKR","WAR","ANK","SWE","SKA","SMY","SYR","SIL","FIN","RUM","NWY","GAL","BER","ARM","CON","DEN","AEG","STP","BAL","BUD","SEV","BLA"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUL R RUM"],"TURKEY":[]},"results":{"A BUL":[],"A VIE":["disband"]},"messages":[]},{"name":"F1911M","state":{"timestamp":1537459323236449,"zobrist_hash":"7251261130085655192","note":"","name":"F1911M","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A LVP","A YOR","F LON"],"FRANCE":[],"GERMANY":["A HOL","A KIE","F BEL","A MUN"],"ITALY":["F ENG","F GRE","A TRI","A SER","A TYR","A VEN","F EAS","A BOH","F NWG","A GAS","F BUL\/SC","F ION"],"RUSSIA":["A SIL","F NWY","A GAL","F BER","F CON","A DEN","F AEG","A STP","F BAL","A BUD","A SEV","F BLA","A RUM"],"TURKEY":[]},"centers":{"AUSTRIA":["BUD"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":[],"GERMANY":["HOL","PAR","BEL","KIE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","MUN","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","BUL","SWE","ANK","CON","SMY","DEN","VIE","BER","NWY"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","EDI","NTH","LVP","YOR","LON"],"FRANCE":[],"GERMANY":["LVN","PIC","RUH","PAR","HEL","HOL","KIE","BEL","MUN"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","ALB","TRI","BUR","BRE","SER","MAO","TYR","NAO","MAR","EAS","BOH","NWG","GAS","BUL","ION"],"RUSSIA":["MOS","BOT","BAR","UKR","WAR","ANK","SWE","SKA","SMY","SYR","SIL","FIN","NWY","GAL","BER","ARM","CON","DEN","AEG","STP","BAL","BUD","SEV","BLA","RUM"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE S A TRI - BUD"],"ENGLAND":["A LVP - EDI","A YOR - LON","F LON - NTH"],"FRANCE":[],"GERMANY":["A HOL - RUH","A KIE S A MUN","F BEL - HOL","A MUN S A KIE"],"ITALY":["F ENG S F NWG - NTH","F GRE S F BUL\/SC - AEG","A TRI - BUD","A SER S A TRI - BUD","A TYR S A VIE","A VEN - TRI","F ION S F BUL\/SC - AEG","A BOH - GAL","A GAS - PAR","F NWG - NTH","F EAS - SMY","F BUL\/SC - AEG"],"RUSSIA":["A SIL - MUN","A GAL - BUD","F NWY - BAR","F BER S A DEN - KIE","F CON S F AEG - BUL","A BUD - SER","F AEG - BUL\/SC","A SEV - UKR","F BLA S F AEG - BUL","A STP - NWY","A DEN - KIE","F BAL S A DEN - KIE","A RUM S A BUD - SER"],"TURKEY":[]},"results":{"A VIE":[],"A LVP":[],"A YOR":["bounce"],"F LON":["bounce"],"A HOL":[],"A KIE":["cut","dislodged"],"F BEL":[],"A MUN":["cut"],"F ENG":[],"F GRE":[],"A TRI":[],"A SER":["cut","dislodged"],"A TYR":[],"A VEN":[],"F EAS":[],"A BOH":["bounce"],"F NWG":[],"A GAS":[],"F BUL\/SC":["bounce"],"F ION":[],"A SIL":["bounce"],"F NWY":[],"A GAL":["bounce"],"F BER":[],"F CON":[],"A DEN":[],"F AEG":["bounce"],"A STP":[],"F BAL":[],"A BUD":[],"A SEV":[],"F BLA":[],"A RUM":[]},"messages":[{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":7037212,"phase":"F1911M","message":"Crap...how many times do I have to have Sevestopol move between the Balkans and the Middle East??????"},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":7087652,"phase":"F1911M","message":"2 more."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7145313,"phase":"F1911M","message":"Thoughts? This is looking more interesting now."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7201424,"phase":"F1911M","message":"I hope you manage to set moves... I have ordered Vienna to support Trieste into Bud just in case that is what you want here."}]},{"name":"F1911R","state":{"timestamp":1537459323239362,"zobrist_hash":"5389844023637212350","note":"","name":"F1911R","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A YOR","F LON","A EDI"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F HOL"],"ITALY":["F ENG","F GRE","A TYR","A BOH","F BUL\/SC","F ION","A BUD","A TRI","F SMY","F NTH","A PAR","*A SER"],"RUSSIA":["A SIL","A GAL","F BER","F CON","F AEG","F BAL","F BLA","A RUM","F BAR","A KIE","A NWY","A SER","A UKR"],"TURKEY":[]},"centers":{"AUSTRIA":["BUD"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":[],"GERMANY":["HOL","PAR","BEL","KIE"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","MUN","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","BUL","SWE","ANK","CON","SMY","DEN","VIE","BER","NWY"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","LVP","YOR","LON","EDI"],"FRANCE":[],"GERMANY":["LVN","PIC","HEL","BEL","MUN","RUH","HOL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","ALB","BUR","BRE","MAO","TYR","NAO","MAR","EAS","BOH","NWG","GAS","BUL","ION","BUD","TRI","SMY","NTH","PAR"],"RUSSIA":["MOS","BOT","WAR","ANK","SWE","SKA","SYR","SIL","FIN","GAL","BER","ARM","CON","DEN","AEG","STP","BAL","SEV","BLA","RUM","BAR","KIE","NWY","SER","UKR"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":["A SER R ALB"],"RUSSIA":[],"TURKEY":[]},"results":{"A SER":[],"A KIE":["disband"]},"messages":[{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7225783,"phase":"F1911R","message":"A decent turn there: two builds - should be enough to get fleets up north."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7247361,"phase":"F1911R","message":"yes indeed! i figured Germany would not have an SC open for a build, but didn't account for him losing kiel. fortunately, that unit is disbanded i think. still, i worry about his reaction to me taking paris."},{"sender":"ITALY","recipient":"GERMANY","time_sent":7249871,"phase":"F1911R","message":"dear germany, i apologize for taking paris without your permission. i was away for the weekend without internet, and when i got on last night just a few minutes before the deadline, i figured you would keep kiel and that paris would be a superfluous SC for you since you can't get a build. i know you are likely to be quite sore at me, but please don't be - i am just trying to get more units into play against the russians and english.<br \/><br \/>now, with two builds, i can send a fleet north which will help a lot i think. as you can see, russia is slowly being pushed back in the south, so that is good news, though no disbands for him as of yet. <br \/><br \/>please let me know what you are thinking and how i can make up paris to you. perhaps an attack on silesia so that you can get some traction against berlin and kiel might be good?"}]},{"name":"W1911A","state":{"timestamp":1537459323240802,"zobrist_hash":"341363097951011687","note":"","name":"W1911A","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A YOR","F LON","A EDI"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F HOL"],"ITALY":["F ENG","F GRE","A TYR","A BOH","F BUL\/SC","F ION","A BUD","A TRI","F SMY","F NTH","A PAR","A ALB"],"RUSSIA":["A SIL","A GAL","F BER","F CON","F AEG","F BAL","F BLA","A RUM","F BAR","A KIE","A NWY","A SER","A UKR"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":[],"GERMANY":["HOL","BEL","MUN"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","BUD","PAR","BUL","SMY"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","ANK","CON","DEN","BER","NWY","KIE","SER"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","LVP","YOR","LON","EDI"],"FRANCE":[],"GERMANY":["LVN","PIC","HEL","BEL","MUN","RUH","HOL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","BUR","BRE","MAO","TYR","NAO","MAR","EAS","BOH","NWG","GAS","BUL","ION","BUD","TRI","SMY","NTH","PAR","ALB"],"RUSSIA":["MOS","BOT","WAR","ANK","SWE","SKA","SYR","SIL","FIN","GAL","BER","ARM","CON","DEN","AEG","STP","BAL","SEV","BLA","RUM","BAR","KIE","NWY","SER","UKR"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":2,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":["A NAP B","F ROM B"],"RUSSIA":[],"TURKEY":[]},"results":{"A NAP":[""],"F ROM":[""]},"messages":[]},{"name":"S1912M","state":{"timestamp":1537459323252935,"zobrist_hash":"5647293268916227992","note":"","name":"S1912M","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A YOR","F LON","A EDI"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F HOL"],"ITALY":["F ENG","F GRE","A TYR","A BOH","F BUL\/SC","F ION","A BUD","A TRI","F SMY","F NTH","A PAR","A ALB","A NAP","F ROM"],"RUSSIA":["A SIL","A GAL","F BER","F CON","F AEG","F BAL","F BLA","A RUM","F BAR","A KIE","A NWY","A SER","A UKR"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":[],"GERMANY":["HOL","BEL","MUN"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","BUD","PAR","BUL","SMY"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","ANK","CON","DEN","BER","NWY","KIE","SER"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","LVP","YOR","LON","EDI"],"FRANCE":[],"GERMANY":["LVN","PIC","HEL","BEL","MUN","RUH","HOL"],"ITALY":["NAP","ROM","TUN","TYS","LYO","WES","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","BUR","BRE","MAO","TYR","NAO","MAR","EAS","BOH","NWG","GAS","BUL","ION","BUD","TRI","SMY","NTH","PAR","ALB"],"RUSSIA":["MOS","BOT","WAR","ANK","SWE","SKA","SYR","SIL","FIN","GAL","BER","ARM","CON","DEN","AEG","STP","BAL","SEV","BLA","RUM","BAR","KIE","NWY","SER","UKR"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE S A BUD"],"ENGLAND":["A YOR - LON","F LON - NTH","A EDI H"],"FRANCE":[],"GERMANY":["A MUN S A SIL - BOH","A RUH S F HOL - BEL","F HOL - BEL"],"ITALY":["F ENG S F LON - NTH","F GRE S F BUL\/SC","A TYR - MUN","F ION C A NAP - ALB","A BOH - GAL","F BUL\/SC - CON","A TRI S A ALB - SER","A PAR - BUR","F NTH - DEN","F SMY S F BUL\/SC - CON","A BUD S A ALB - SER","A ALB - SER","A NAP - ALB VIA","F ROM - TYS"],"RUSSIA":["A SIL S A GAL","A GAL S A SER - BUD","F BER S A KIE","F CON S F AEG - BUL","F AEG - BUL\/SC","F BLA S F AEG - BUL","F BAL - DEN","A RUM S A SER - BUD","A UKR S A GAL","A NWY - SWE","F BAR - NWY","A KIE H","A SER - BUD"],"TURKEY":[]},"results":{"A VIE":[],"A YOR":["bounce"],"F LON":["bounce"],"A EDI":[],"A MUN":["void"],"A RUH":[],"F HOL":[],"F ENG":[],"F GRE":["void"],"A TYR":["bounce"],"A BOH":["bounce"],"F BUL\/SC":[],"F ION":[],"A BUD":[],"A TRI":[],"F SMY":[],"F NTH":["bounce"],"A PAR":[],"A ALB":[],"A NAP":[],"F ROM":[],"A SIL":[],"A GAL":["cut"],"F BER":[],"F CON":["cut","dislodged"],"F AEG":[],"F BAL":["bounce"],"F BLA":[],"A RUM":[],"F BAR":[],"A KIE":[],"A NWY":[],"A SER":["bounce","dislodged"],"A UKR":[]},"messages":[{"sender":"GERMANY","recipient":"RUSSIA","time_sent":7259344,"phase":"S1912M","message":"Italy has made an ENORMOUS error judgement.<br \/><br \/>Where do you want me to move?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":7259479,"phase":"S1912M","message":"my northern fleets are now at your disposal - what would you like them to do? same with my army in paris."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":7260061,"phase":"S1912M","message":"seems like things are still very much in flux, but we will see some resolution soon."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":7260077,"phase":"S1912M","message":"or did you have any proposals in mind?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":7260203,"phase":"S1912M","message":"is this your sitter again? or are you just being quiet now too?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":7260267,"phase":"S1912M","message":"it seems like it might be good for me to try for denamark. or, i can move to hel. or do soemthing else. let me know what would be most helpful from your perspective."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":7261193,"phase":"S1912M","message":"Blowing up Bohemia might be a good start....."},{"sender":"GERMANY","recipient":"ITALY","time_sent":7261247,"phase":"S1912M","message":"Please order Par-Bur."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":7261371,"phase":"S1912M","message":"I wish to support Sil-Boh. Italy will not recover from the loss of the army in the spring."},{"sender":"ITALY","recipient":"GERMANY","time_sent":7261456,"phase":"S1912M","message":"par-bur? done.<br \/><br \/>and what would you like Nth and EC to do?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":7261479,"phase":"S1912M","message":"i would like to finally finish off the english, but i need an additional fleet to manage that, i think."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":7261834,"phase":"S1912M","message":"Tag...you're it."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":7261870,"phase":"S1912M","message":"haha."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":7265523,"phase":"S1912M","message":"So, do I gain some favor now that Italy is number 1?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":7272554,"phase":"S1912M","message":"Still me but still not able to get on very much."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":7272741,"phase":"S1912M","message":"not even enough to send press?<br \/><br \/>what do you want to do this year? are you going to keep fighting me or can we put our differences aside and take on russia?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7285754,"phase":"S1912M","message":"Russia is asking me to switch sides. I will not do so but you can assume he is also talking to Germany now."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":7289448,"phase":"S1912M","message":"If you can make use of my fleet I'm more than willing to share"},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":7296410,"phase":"S1912M","message":"So you want to be friends for like the 4th time? Looks like you will have to support RUH-BEL. It wouldn't surprise me if he tried to convoy an English unit there, as well. I really don't relish leaving KIE and BER vulnerable. Nor do I relish watching another turn go by to find out you didn't follow through."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7298818,"phase":"S1912M","message":"i am sure."},{"sender":"ITALY","recipient":"GERMANY","time_sent":7339377,"phase":"S1912M","message":"are you going to work with russia now?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":7340046,"phase":"S1912M","message":"Russia is dead set on going it alone. He\/she is sitting in Ber and Kie without my help or blessing anyway."},{"sender":"ITALY","recipient":"GERMANY","time_sent":7340469,"phase":"S1912M","message":"ok, glad to keep working with you. what do you want me to do with nth and EC? i can try for Den, i guess. and do you want me to use Boh for something?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":7357369,"phase":"S1912M","message":"Please hit Mun with Boh. Don't support it please."},{"sender":"GERMANY","recipient":"ITALY","time_sent":7357386,"phase":"S1912M","message":"Also, please hit Den."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":7357406,"phase":"S1912M","message":"Fine. I will not trouble you again."},{"sender":"ITALY","recipient":"GERMANY","time_sent":7357459,"phase":"S1912M","message":"ok, done and done."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":7357478,"phase":"S1912M","message":"i could support you into north sea? i will be vacating it."},{"sender":"ITALY","recipient":"GERMANY","time_sent":7357602,"phase":"S1912M","message":"thought i was hoping to use boh to hit gal. what's the purpose of using boh to hit mun? can it be accomplished any other way? maybe tyr can hit mun?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7357709,"phase":"S1912M","message":"alright, this turn? i'm going to try to retake serbia - can you use vienna to support hold budapest? it seems that germany is not interested in working with russia, tho it's hard to tell. and england may finally be coming around."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":7366155,"phase":"S1912M","message":"Then I could go to the North Sea, Will Germany be civil or will we have to worry about him losing too many SC's? Great job below I don't know what happened while I was gone but you managed to pull off some awesome work."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7386575,"phase":"S1912M","message":"I will support Budapest."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":7388171,"phase":"S1912M","message":"i'm not exactly sure how i managed it either, but it is good. i think germany will be civil, yes, but you know it's always hard to say."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7426415,"phase":"S1912M","message":"thank you kindly. i think i'll finalize, then."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":7426441,"phase":"S1912M","message":"i am supporting Lon-Nth. take it or leave it, it's fine with me."},{"sender":"GERMANY","recipient":"ITALY","time_sent":7428308,"phase":"S1912M","message":"If you want. I just need an unsupported attack on Mun."},{"sender":"ITALY","recipient":"GERMANY","time_sent":7428453,"phase":"S1912M","message":"you got it."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":7451435,"phase":"S1912M","message":"It's being taken."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":7480413,"phase":"S1912M","message":"great."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":7515964,"phase":"S1912M","message":"Care to discuss why you are leaving the League? I left the Premier recently and TGM was the reason."}]},{"name":"S1912R","state":{"timestamp":1537459323255825,"zobrist_hash":"5368173680361726290","note":"","name":"S1912R","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A YOR","F LON","A EDI"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F BEL"],"ITALY":["F ENG","F GRE","A TYR","A BOH","F ION","A BUD","A TRI","F SMY","F NTH","F CON","A BUR","A SER","A ALB","F TYS"],"RUSSIA":["A SIL","A GAL","F BER","F BAL","F BLA","A RUM","A KIE","A UKR","F BUL\/SC","F NWY","A SWE","*F CON"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":[],"GERMANY":["HOL","BEL","MUN"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","BUD","PAR","BUL","SMY"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","ANK","CON","DEN","BER","NWY","KIE","SER"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","LVP","YOR","LON","EDI"],"FRANCE":[],"GERMANY":["LVN","PIC","HEL","MUN","RUH","HOL","BEL"],"ITALY":["NAP","ROM","TUN","LYO","WES","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","BRE","MAO","TYR","NAO","MAR","EAS","BOH","NWG","GAS","ION","BUD","TRI","SMY","NTH","PAR","CON","BUR","SER","ALB","TYS"],"RUSSIA":["MOS","BOT","WAR","ANK","SKA","SYR","SIL","FIN","GAL","BER","ARM","DEN","AEG","STP","BAL","SEV","BLA","RUM","BAR","KIE","UKR","BUL","NWY","SWE"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["F CON R ANK"],"TURKEY":[]},"results":{"F CON":[],"A SER":["disband"]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7517866,"phase":"S1912R","message":"excellent!"},{"sender":"ITALY","recipient":"GERMANY","time_sent":7517906,"phase":"S1912R","message":"huh, i don't understand your moves? you are working with the russia now, it would seem?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":7517963,"phase":"S1912R","message":"well that didn't go so well, but things in the south did!"}]},{"name":"F1912M","state":{"timestamp":1537459323267466,"zobrist_hash":"7405273088180126967","note":"","name":"F1912M","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A YOR","F LON","A EDI"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F BEL"],"ITALY":["F ENG","F GRE","A TYR","A BOH","F ION","A BUD","A TRI","F SMY","F NTH","F CON","A BUR","A SER","A ALB","F TYS"],"RUSSIA":["A SIL","A GAL","F BER","F BAL","F BLA","A RUM","A KIE","A UKR","F BUL\/SC","F NWY","A SWE","F ANK"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":[],"GERMANY":["HOL","BEL","MUN"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","BUD","PAR","BUL","SMY"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","ANK","CON","DEN","BER","NWY","KIE","SER"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","LVP","YOR","LON","EDI"],"FRANCE":[],"GERMANY":["LVN","PIC","HEL","MUN","RUH","HOL","BEL"],"ITALY":["NAP","ROM","TUN","LYO","WES","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","BRE","MAO","TYR","NAO","MAR","EAS","BOH","NWG","GAS","ION","BUD","TRI","SMY","NTH","PAR","CON","BUR","SER","ALB","TYS"],"RUSSIA":["MOS","BOT","WAR","SKA","SYR","SIL","FIN","GAL","BER","ARM","DEN","AEG","STP","BAL","SEV","BLA","RUM","BAR","KIE","UKR","BUL","NWY","SWE","ANK"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE S A BOH - GAL"],"ENGLAND":["A YOR H","F LON H","A EDI H"],"FRANCE":[],"GERMANY":["A MUN S A RUH","A RUH S A MUN","F BEL - HOL"],"ITALY":["F ENG S F NTH - LON","F GRE S A SER - BUL","A TYR - MUN","F ION - AEG","A BOH - GAL","A TRI S A BUD","F NTH - LON","F SMY S F CON","A BUD S A ALB - SER","A ALB - SER","F TYS - WES","A SER - BUL","A BUR H","F CON S A SER - BUL"],"RUSSIA":["A SIL - BOH","A GAL - BUD","F BER S A KIE","F BLA S F BUL\/SC","F BAL - DEN","A RUM S F BUL\/SC","A UKR S A RUM","A KIE S A MUN","A SWE S F BAL - DEN","F NWY H","F BUL\/SC H","F ANK - CON"],"TURKEY":[]},"results":{"A VIE":[],"A YOR":[],"F LON":["dislodged"],"A EDI":[],"A MUN":["cut"],"A RUH":[],"F BEL":[],"F ENG":[],"F GRE":[],"A TYR":["bounce"],"A BOH":[],"F ION":[],"A BUD":["cut"],"A TRI":[],"F SMY":[],"F NTH":[],"F CON":["cut"],"A BUR":[],"A SER":["bounce"],"A ALB":["bounce"],"F TYS":[],"A SIL":[],"A GAL":["bounce","dislodged"],"F BER":[],"F BAL":[],"F BLA":[],"A RUM":[],"A KIE":[],"A UKR":[],"F BUL\/SC":[],"F NWY":[],"A SWE":[],"F ANK":["bounce"]},"messages":[{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":7594899,"phase":"F1912M","message":"Also, try the Forum: there is a game just made for you... It requires a lot of cursing and the like: a perfect opportunity to sound off. I have joined."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":7594929,"phase":"F1912M","message":"gameID=15895"},{"sender":"GERMANY","recipient":"ITALY","time_sent":7597175,"phase":"F1912M","message":"No, I just messed up(a familiar refrain for me this game, sadly).<br \/><br \/>I had considered it, but was rebuffed by Russia. I did not change my orders."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":7597204,"phase":"F1912M","message":"A nadir for me. I regret the posting."},{"sender":"ITALY","recipient":"GERMANY","time_sent":7597674,"phase":"F1912M","message":"what exactly were you trying to do? and what would you like to do this turn?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7597959,"phase":"F1912M","message":"that went well! thoughts for this turn? you're safely ensconced in vienna now =)"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":7597978,"phase":"F1912M","message":"shall we try again? maybe i'll go to ska this tume?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7605808,"phase":"F1912M","message":"Entirely at your mercy, of course."},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":7606559,"phase":"F1912M","message":"From what I am being told, your leaving the League is based on the same sort of problem one of my games had. In my case though, it happened to Autumn 1901, England and Germany proffitted massively as they had already jointly moved against France, which missed the chance to move through the turn jumping to the next phase (so, no builds first year against a joint attack: Game Over basically)... England insisted that the game must continue unless everyone voted to stop it - knowing that Germany had already refused to stop... and, of course, England being none other than TGM himself.<br \/><br \/>It doesn't get much worse in my book. No official, no Game Master, must ever be seen to even come close to an advantage caused by an external factor. That's it as far as I'm concerned. I pulled out of the League and am out of all the remaining games of the Masters too."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7671545,"phase":"F1912M","message":"Are you moving on Galicia?"},{"sender":"GERMANY","recipient":"ITALY","time_sent":7685352,"phase":"F1912M","message":"You and Russia are dividing the board, while attempting to convey the illusion of opposing each other. <br \/><br \/>You both have my compliments.<br \/><br \/>Do not insult me further with these messages please.<br \/><br \/>Enough is enough."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7687371,"phase":"F1912M","message":"sure. would you mind supporting boh-gal?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":7687452,"phase":"F1912M","message":"actually that is not at all what is going on. but i know you well enough from this game that once you believe something, you stick to it, so i'm not sure if it's worth trying to convince you otherwise.<br \/><br \/>still, i would like to keep working with you. your armies are really strategically placed."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7688209,"phase":"F1912M","message":"No problem..."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7688233,"phase":"F1912M","message":"Done..."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7688820,"phase":"F1912M","message":"thanks!"},{"sender":"ITALY","recipient":"GERMANY","time_sent":7691744,"phase":"F1912M","message":"i am going to tap you at munich again, to dissuade you from forcing burgundy. it will be an unsupported attack."}]},{"name":"F1912R","state":{"timestamp":1537459323269554,"zobrist_hash":"1065788704169095443","note":"","name":"F1912R","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A YOR","A EDI","*F LON"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F HOL"],"ITALY":["F ENG","F GRE","A TYR","A BUD","A TRI","F SMY","F CON","A BUR","A SER","A ALB","A GAL","F AEG","F LON","F WES"],"RUSSIA":["F BER","F BLA","A RUM","A KIE","A UKR","F BUL\/SC","F NWY","A SWE","F ANK","A BOH","F DEN","*A GAL"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":[],"GERMANY":["HOL","BEL","MUN"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","BUD","PAR","BUL","SMY"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","ANK","CON","DEN","BER","NWY","KIE","SER"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","LVP","YOR","EDI"],"FRANCE":[],"GERMANY":["LVN","PIC","HEL","MUN","RUH","BEL","HOL"],"ITALY":["NAP","ROM","TUN","LYO","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","BRE","MAO","TYR","NAO","MAR","EAS","NWG","GAS","ION","BUD","TRI","SMY","NTH","PAR","CON","BUR","SER","ALB","TYS","GAL","AEG","LON","WES"],"RUSSIA":["MOS","BOT","WAR","SKA","SYR","SIL","FIN","BER","ARM","STP","BAL","SEV","BLA","RUM","BAR","KIE","UKR","BUL","NWY","SWE","ANK","BOH","DEN"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F LON D"],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["A GAL R WAR"],"TURKEY":[]},"results":{"F LON":["disband"],"A GAL":[]},"messages":[{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7735017,"phase":"F1912R","message":"Looks like England has given up here. Your win."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7735057,"phase":"F1912R","message":"Let me guess, Galicia to Ukraine?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7735086,"phase":"F1912R","message":"And Serbia to Rum..."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":7779070,"phase":"F1912R","message":"Nice work."},{"sender":"ITALY","recipient":"GERMANY","time_sent":7860580,"phase":"F1912R","message":"do you believe me yet that i am not actually working with russia?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7860710,"phase":"F1912R","message":"that would be nice.<br \/><br \/>thanks for your help that turn. i hope this game can end soon - i have too many games. ser to rum is one good option, but so is taking bul. i'll have to see which makes more sense. would you be so kind as to support me into boh?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":7860742,"phase":"F1912R","message":"if you would be up for it, i would take your support into nth sea this turn? and let me know if there are any ways i can help you to retake your SCs."}]},{"name":"W1912A","state":{"timestamp":1537459323270925,"zobrist_hash":"3365397855196591644","note":"","name":"W1912A","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A YOR","A EDI"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F HOL"],"ITALY":["F ENG","F GRE","A TYR","A BUD","A TRI","F SMY","F CON","A BUR","A SER","A ALB","A GAL","F AEG","F LON","F WES"],"RUSSIA":["F BER","F BLA","A RUM","A KIE","A UKR","F BUL\/SC","F NWY","A SWE","F ANK","A BOH","F DEN","A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LVP"],"FRANCE":[],"GERMANY":["HOL","BEL","MUN"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","BUD","PAR","SMY","LON","CON","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","ANK","DEN","BER","NWY","KIE","BUL"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","LVP","YOR","EDI"],"FRANCE":[],"GERMANY":["LVN","PIC","HEL","MUN","RUH","BEL","HOL"],"ITALY":["NAP","ROM","TUN","LYO","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","BRE","MAO","TYR","NAO","MAR","EAS","NWG","GAS","ION","BUD","TRI","SMY","NTH","PAR","CON","BUR","SER","ALB","TYS","GAL","AEG","LON","WES"],"RUSSIA":["MOS","BOT","SKA","SYR","SIL","FIN","BER","ARM","STP","BAL","SEV","BLA","RUM","BAR","KIE","UKR","BUL","NWY","SWE","ANK","BOH","DEN","WAR"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":2,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":["A ROM B","A VEN B"],"RUSSIA":[],"TURKEY":[]},"results":{"A ROM":[""],"A VEN":[""]},"messages":[]},{"name":"S1913M","state":{"timestamp":1537459323281427,"zobrist_hash":"2284628193587632703","note":"","name":"S1913M","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A YOR","A EDI"],"FRANCE":[],"GERMANY":["A MUN","A RUH","F HOL"],"ITALY":["F ENG","F GRE","A TYR","A BUD","A TRI","F SMY","F CON","A BUR","A SER","A ALB","A GAL","F AEG","F LON","F WES","A ROM","A VEN"],"RUSSIA":["F BER","F BLA","A RUM","A KIE","A UKR","F BUL\/SC","F NWY","A SWE","F ANK","A BOH","F DEN","A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LVP"],"FRANCE":[],"GERMANY":["HOL","BEL","MUN"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","BUD","PAR","SMY","LON","CON","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","ANK","DEN","BER","NWY","KIE","BUL"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","LVP","YOR","EDI"],"FRANCE":[],"GERMANY":["LVN","PIC","HEL","MUN","RUH","BEL","HOL"],"ITALY":["NAP","ROM","TUN","LYO","APU","POR","ADR","IRI","VEN","ENG","PIE","SPA","GRE","BRE","MAO","TYR","NAO","MAR","EAS","NWG","GAS","ION","BUD","TRI","SMY","NTH","PAR","CON","BUR","SER","ALB","TYS","GAL","AEG","LON","WES"],"RUSSIA":["MOS","BOT","SKA","SYR","SIL","FIN","BER","ARM","STP","BAL","SEV","BLA","RUM","BAR","KIE","UKR","BUL","NWY","SWE","ANK","BOH","DEN","WAR"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE S A TYR - BOH"],"ENGLAND":["A YOR H","A EDI H"],"FRANCE":[],"GERMANY":["A MUN H","A RUH - BEL","F HOL S A RUH - BEL"],"ITALY":["F ENG - IRI","F GRE S F AEG - BUL","A TYR - BOH","A TRI S A BUD","F SMY - CON","A BUD S A SER - RUM","A ALB - SER","A SER - RUM","A BUR - MUN","F CON - BLA","F LON - NTH","F WES - MAO","F AEG - BUL\/SC","A GAL - UKR","A ROM - VEN","A VEN - PIE"],"RUSSIA":["F BER - BAL","F BLA S F BUL\/SC - CON","A RUM H","A UKR S A RUM","A KIE S A MUN","A SWE - DEN","F NWY S F DEN - NTH","F BUL\/SC - CON","F ANK S F BUL\/SC - CON","A BOH - SIL","F DEN - NTH","A WAR - SIL"],"TURKEY":[]},"results":{"A VIE":[],"A YOR":[],"A EDI":[],"A MUN":[],"A RUH":[],"F HOL":[],"F ENG":[],"F GRE":[],"A TYR":[],"A BUD":[],"A TRI":[],"F SMY":["bounce"],"F CON":["bounce","dislodged"],"A BUR":["bounce"],"A SER":[],"A ALB":[],"A GAL":["bounce"],"F AEG":[],"F LON":["bounce"],"F WES":[],"A ROM":[],"A VEN":[],"F BER":[],"F BLA":[],"A RUM":["dislodged"],"A KIE":[],"A UKR":["cut"],"F BUL\/SC":[],"F NWY":[],"A SWE":[],"F ANK":[],"A BOH":["bounce","dislodged"],"F DEN":[],"A WAR":["bounce"]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7879621,"phase":"S1913M","message":"seems like we lost the englishman. are we still working together now, or do i lose my stalwart friend this turn?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":7879674,"phase":"S1913M","message":"seems like you should be able to retake kiel, actually. i'll keep russia on his toes in the meantime..."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7879701,"phase":"S1913M","message":"if i can count on your help, let me know what you'd like to do."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7890030,"phase":"S1913M","message":"I can support you into Boh, no problem.<br \/><br \/>Tyrolia to Boh."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":7941386,"phase":"S1913M","message":"Do you want to HAND the game to Italy?"},{"sender":"GERMANY","recipient":"GLOBAL","time_sent":7941522,"phase":"S1913M","message":"Good game, bent."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":7942637,"phase":"S1913M","message":"Are you ready for it to be over? I am doing what I can here....I am outnumbered in the Balkans so I don't expect to be able to stop her. What do you suggest? And if the game gets handed to her it will be by more than one hand. Austria won't deal with me and you seem only willing after she has taken more of your space."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":8027879,"phase":"S1913M","message":"All I need is for you to keep supporting my army in Mun. I've been trying to get some people on board with me versus Italy since the game started. NOBODY attacked her."},{"sender":"ITALY","recipient":"GERMANY","time_sent":8032254,"phase":"S1913M","message":"any chance of support for me into the north sea? any other chance of us working together?"}]},{"name":"S1913R","state":{"timestamp":1537459323293594,"zobrist_hash":"4689544996940951632","note":"","name":"S1913R","units":{"AUSTRIA":["A VIE"],"ENGLAND":["A YOR","A EDI"],"FRANCE":[],"GERMANY":["A MUN","F HOL","A BEL"],"ITALY":["F GRE","A BUD","A TRI","F SMY","A BUR","A GAL","F LON","F IRI","A BOH","A RUM","A SER","F BUL\/SC","F MAO","A VEN","A PIE","*F CON"],"RUSSIA":["F BLA","A KIE","A UKR","F NWY","F ANK","A WAR","F BAL","F CON","A DEN","F NTH","*A RUM"],"TURKEY":[]},"centers":{"AUSTRIA":["VIE"],"ENGLAND":["EDI","LVP"],"FRANCE":[],"GERMANY":["HOL","BEL","MUN"],"ITALY":["NAP","ROM","TUN","MAR","POR","SPA","VEN","TRI","GRE","BRE","BUD","PAR","SMY","LON","CON","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","ANK","DEN","BER","NWY","KIE","BUL"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE"],"ENGLAND":["CLY","WAL","LVP","YOR","EDI"],"FRANCE":[],"GERMANY":["LVN","PIC","HEL","MUN","RUH","HOL","BEL"],"ITALY":["NAP","ROM","TUN","LYO","APU","POR","ADR","ENG","SPA","GRE","BRE","TYR","NAO","MAR","EAS","NWG","GAS","ION","BUD","TRI","SMY","PAR","BUR","ALB","TYS","GAL","AEG","LON","WES","IRI","BOH","RUM","SER","BUL","MAO","VEN","PIE"],"RUSSIA":["MOS","BOT","SKA","SYR","SIL","FIN","BER","ARM","STP","SEV","BLA","BAR","KIE","UKR","NWY","SWE","ANK","WAR","BAL","CON","DEN","NTH"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"00128f1d","map":"standard","rules":[]},"orders":{"AUSTRIA":null,"ENGLAND":null,"FRANCE":[],"GERMANY":null,"ITALY":null,"RUSSIA":null,"TURKEY":[]},"results":{},"messages":[]}]}
\ No newline at end of file diff --git a/diplomacy/tests/network/2.json b/diplomacy/tests/network/2.json new file mode 100644 index 0000000..8e06c6e --- /dev/null +++ b/diplomacy/tests/network/2.json @@ -0,0 +1 @@ +{"id":"001ce02c","map":"standard","rules":[],"phases":[{"name":"S1901M","state":{"timestamp":1537459327370719,"zobrist_hash":"6621580922936090403","note":"","name":"S1901M","units":{"AUSTRIA":["A BUD","A VIE","F TRI"],"ENGLAND":["F EDI","F LON","A LVP"],"FRANCE":["F BRE","A MAR","A PAR"],"GERMANY":["F KIE","A BER","A MUN"],"ITALY":["F NAP","A ROM","A VEN"],"RUSSIA":["A WAR","A MOS","F SEV","F STP\/SC"],"TURKEY":["F ANK","A CON","A SMY"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","VIE","TRI"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["KIE","BER","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["WAR","MOS","SEV","STP"],"TURKEY":["ANK","CON","SMY"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD - RUM","A VIE - BUD","F TRI - ALB"],"ENGLAND":["F EDI - NTH","F LON - ENG","A LVP - YOR"],"FRANCE":["F BRE - MAO","A MAR S A PAR - BUR","A PAR - BUR"],"GERMANY":["F KIE - DEN","A BER - KIE","A MUN - RUH"],"ITALY":["F NAP - ION","A ROM - APU","A VEN H"],"RUSSIA":["A WAR - GAL","A MOS - UKR","F SEV - BLA","F STP\/SC - BOT"],"TURKEY":["F ANK - BLA","A CON - BUL","A SMY - ARM"]},"results":{"A BUD":[],"A VIE":[],"F TRI":[],"F EDI":[],"F LON":[],"A LVP":[],"F BRE":[],"A MAR":[],"A PAR":[],"F KIE":[],"A BER":[],"A MUN":[],"F NAP":[],"A ROM":[],"A VEN":[],"A WAR":[],"A MOS":[],"F SEV":["bounce"],"F STP\/SC":[],"F ANK":["bounce"],"A CON":[],"A SMY":[]},"messages":[{"sender":"AUSTRIA","recipient":"ITALY","time_sent":0,"phase":"S1901M","message":"Hey Italy, I am going to have my hands full with Turkey, Germany and Russia. It's not worth it to me to come after you."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7,"phase":"S1901M","message":"What do you think?"},{"sender":"AUSTRIA","recipient":"GERMANY","time_sent":394,"phase":"S1901M","message":"Let's talk Germany. I would be into an anti Russian alliance. What say you?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":940,"phase":"S1901M","message":"Hi England. How about a dmz in the channel?"},{"sender":"FRANCE","recipient":"GERMANY","time_sent":969,"phase":"S1901M","message":"hey there! Would you be interested in a fair share of holand and belgium?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":983,"phase":"S1901M","message":"Hi, how about a dmz in piedmont?"},{"sender":"GERMANY","recipient":"FRANCE","time_sent":1746,"phase":"S1901M","message":"Yes that would be good."},{"sender":"GERMANY","recipient":"AUSTRIA","time_sent":1775,"phase":"S1901M","message":"Yes that would be very good."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":1975,"phase":"S1901M","message":"What are your plans? Team up on Germany then?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":2241,"phase":"S1901M","message":"Hello. I appreciate the offer. Would you consider increasing the scope of our agreement?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":2426,"phase":"S1901M","message":"I think that it is essential that we not attack one another. In short, I love your proposal and accept!. But I also want the Austrian\/Italian relationship to blossom into something even bigger. Like an alliance where we trust each other explicitly and move in tandem against our foes as one unit."},{"sender":"ITALY","recipient":"GERMANY","time_sent":2583,"phase":"S1901M","message":"Greetings. I hope that the two of us will enjoy a mutually beneficial exchange of information and maybe even cooperative joint ventures at some point."},{"sender":"ITALY","recipient":"TURKEY","time_sent":2622,"phase":"S1901M","message":"Hello. Please keep me in the loop of your plans."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2729,"phase":"S1901M","message":"I hope that the two of us will exchange of information about developing alliances and plans. I see no downside."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2799,"phase":"S1901M","message":"I hope that the two of us will enjoy a mutually beneficial exchange of information. Even more, I believe Russia and Italy constitute a strong strategic alliance and I hope it comes to fruition in this game."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":3636,"phase":"S1901M","message":"In my opinion, game long alliances like that are the best and sometimes only way to win games. I am in."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":3695,"phase":"S1901M","message":"What do you want to do on this 1st turn?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4162,"phase":"S1901M","message":"Agreed. Good luck"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":4174,"phase":"S1901M","message":"Juggernaught!? lets dmz BS"},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":4182,"phase":"S1901M","message":"peace? lets dmz gal"},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":4206,"phase":"S1901M","message":"lets have peace and dmz you 2 east territories. Ill try to avoid a build in war as well"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":4213,"phase":"S1901M","message":"lets have peace"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":4462,"phase":"S1901M","message":"Will you go after Turkey if we make peace?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4592,"phase":"S1901M","message":"I'm not sure yet. It depends on what we think Russia and Turkey will do I think."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":4758,"phase":"S1901M","message":"Do you want to try something wild?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4779,"phase":"S1901M","message":"I could be convinced"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":4793,"phase":"S1901M","message":"What do you have in mind?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":5062,"phase":"S1901M","message":"Maybe a blue water lepanto. This would only be if we decide we want to attack the Turk."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":5279,"phase":"S1901M","message":"I am open to that DMZ"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5372,"phase":"S1901M","message":"We could do that. So this turn you want me to move to the Adriatic"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5381,"phase":"S1901M","message":"Move to trieste"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":5390,"phase":"S1901M","message":"and move to Serbia?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":5431,"phase":"S1901M","message":"in how far? I would stay out of the mediterranean. let me know if I can actually help you with anything"},{"sender":"FRANCE","recipient":"GERMANY","time_sent":5474,"phase":"S1901M","message":"so you take holland, i take belgium and we don't have to fight each other?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":6545,"phase":"S1901M","message":"Agree with no downside. What's the word with France? So far they've just asked to DMZ the Channel."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":6560,"phase":"S1901M","message":"Join against Germany then?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":6697,"phase":"S1901M","message":"I was thinking of maybe asking if you would consider expanding the proposal to ban fleets from landing in Spain(sc) or being built in Mar. And yes, French fleets in the Med are not good for Italian health."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6746,"phase":"S1901M","message":"He must like a lot of dmzs, ha-ha."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":6757,"phase":"S1901M","message":"... if you get my drift."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":7137,"phase":"S1901M","message":"Well, that is the original version, but moving to Adr seems unnecessarily. You can move the fleet to Ionian Sea in the fall from Albania just as well as Adriatic. So I see no reason to do something weird like Tri-Adr that might alert a savvy player who has read the article."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":7677,"phase":"S1901M","message":"Hmm I can do that. And what will you do?"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":8468,"phase":"S1901M","message":"ya. i dont think i can do an early push but maybe.."},{"sender":"FRANCE","recipient":"ITALY","time_sent":8480,"phase":"S1901M","message":"oh, sure, there will be no fleet-building in spain or marseilles ,don't worry about that"},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":8486,"phase":"S1901M","message":"sure. whatd you have in mind"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":8545,"phase":"S1901M","message":"I'll open A Rom-Apu. A Ven H or I will move to Piedmont or maybe even Tyrolia, depending on the French and western negotiations."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":8552,"phase":"S1901M","message":"ill sup your fleet from con-bul in the fall"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":8557,"phase":"S1901M","message":"bul (SC)"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":8760,"phase":"S1901M","message":":)"},{"sender":"ITALY","recipient":"FRANCE","time_sent":8775,"phase":"S1901M","message":"Thanks. I understand that if you agree that if you must move a fleet to Spain, you will move it to Spain(nc). If that is understood, then we have a dmz deal!"},{"sender":"FRANCE","recipient":"ITALY","time_sent":8967,"phase":"S1901M","message":"that's perfectly fine with me"},{"sender":"GERMANY","recipient":"FRANCE","time_sent":12400,"phase":"S1901M","message":"Yes agreed."},{"sender":"GERMANY","recipient":"ITALY","time_sent":12448,"phase":"S1901M","message":"Yes, that would be good."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":12497,"phase":"S1901M","message":"Yes that would be good, agreed."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":12665,"phase":"S1901M","message":"I believe Austria maybe moving to Galicia in a Turkish alliance, but I am not 100% certain if this is true or just a distraction."},{"sender":"ITALY","recipient":"FRANCE","time_sent":12970,"phase":"S1901M","message":"Good start."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":18898,"phase":"S1901M","message":"thanks"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":22169,"phase":"S1901M","message":"Based on what I'm hearing, it sounds like the French and English are getting along well."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":25608,"phase":"S1901M","message":"as are turk\/aus so we need to watch out"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":67376,"phase":"S1901M","message":"I think we are in a good position."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":70092,"phase":"S1901M","message":"I have an agreement with France that he will stay out of positions that threaten the Med if I stay out of Piedmont. We will see. If that holds, our plans to move aggressively against Turkey should be fine. We should see if we can get Russia onboard. He has been circumspect with me. What is he telling you?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":70235,"phase":"S1901M","message":"Agreed. Do you have a preference for which one we should target? Do you feel good about Austria working with us?."},{"sender":"ITALY","recipient":"GERMANY","time_sent":70309,"phase":"S1901M","message":"Any news? I hear that there are negotiations to demilitarize the Channel."},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":71966,"phase":"S1901M","message":"DMZ Gal for sure. I will go after Turkey hard"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":74545,"phase":"S1901M","message":"im worried about both but understand if you need to stay on Aus's good side"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":74586,"phase":"S1901M","message":"I cnat hit Ger - turk is silent and ive heard him and AUS are together"},{"sender":"GERMANY","recipient":"ITALY","time_sent":82940,"phase":"S1901M","message":"I have heard nothing from England, I don't think France is going to attack you and Austria is moving against Russia. I think France will go for a quick 3 centre gain, Spain, Portugal, Belgium, which might be why you heard of a DMZ in the channel, but I can't confirm the DMZ."},{"sender":"ITALY","recipient":"GERMANY","time_sent":84208,"phase":"S1901M","message":"England wrote to me. Not sure that bodes well for Germany. If they attack you, I see no reason why I should not help you fight them off. An E\/F is not something that is good for long-term Italian health. <br \/><br \/>I know of no Austrian plans to attack Russia. I think they might bounce in Gal, but Austria is just going for his Balkan dots I think. I doubt he moves to Rum. But stranger things have been known to happen."}]},{"name":"F1901M","state":{"timestamp":1537459327379276,"zobrist_hash":"1508767767776253387","note":"","name":"F1901M","units":{"AUSTRIA":["A RUM","A BUD","F ALB"],"ENGLAND":["F NTH","F ENG","A YOR"],"FRANCE":["A MAR","F MAO","A BUR"],"GERMANY":["F DEN","A KIE","A RUH"],"ITALY":["A VEN","F ION","A APU"],"RUSSIA":["F SEV","A GAL","A UKR","F BOT"],"TURKEY":["F ANK","A BUL","A ARM"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","TRI","RUM","BUD","ALB"],"ENGLAND":["EDI","LON","LVP","NTH","ENG","YOR"],"FRANCE":["BRE","MAR","PAR","MAO","BUR"],"GERMANY":["BER","MUN","DEN","KIE","RUH"],"ITALY":["NAP","ROM","VEN","ION","APU"],"RUSSIA":["WAR","MOS","SEV","STP","GAL","UKR","BOT"],"TURKEY":["ANK","CON","SMY","BUL","ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD S A RUM","F ALB - ION","A RUM S A BUD"],"ENGLAND":["F NTH C A YOR - NWY","A YOR - NWY VIA","F ENG - BRE"],"FRANCE":["A MAR - SPA","F MAO - POR","A BUR - MUN"],"GERMANY":["F DEN H","A KIE - HOL","A RUH - MUN"],"ITALY":["A VEN - TYR","F ION - TUN","A APU - VEN"],"RUSSIA":["F SEV S A UKR - RUM","A GAL - BUD","A UKR - RUM","F BOT - SWE"],"TURKEY":["F ANK - BLA","A BUL - SER","A ARM H"]},"results":{"A RUM":["cut","dislodged"],"A BUD":["cut"],"F ALB":[],"F NTH":[],"F ENG":[],"A YOR":[],"A MAR":[],"F MAO":[],"A BUR":["bounce"],"F DEN":[],"A KIE":[],"A RUH":["bounce"],"A VEN":[],"F ION":[],"A APU":[],"F SEV":[],"A GAL":["bounce"],"A UKR":[],"F BOT":[],"F ANK":[],"A BUL":[],"A ARM":[]},"messages":[{"sender":"ENGLAND","recipient":"GERMANY","time_sent":86098,"phase":"F1901M","message":"Are you planning the typical Sweden bounce with Russia? With them committing to the south, it'd be nice to restrict them in the north."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":86228,"phase":"F1901M","message":"Turkey's Armenia move is pretty interesting."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":86291,"phase":"F1901M","message":"Also, I'll assume you'll want to team up against France, especially after their move to Bur."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":87820,"phase":"F1901M","message":"its jsut shows how aus and turk are allies now"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":87826,"phase":"F1901M","message":"Well, thank you..."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":88167,"phase":"F1901M","message":"It does look like it from that first move. If you're fine giving up Black Sea, you could try taking Rum to see if Turkey will help hold it."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":88251,"phase":"F1901M","message":"It should be for Bel; we'll see what Germany does. I assume you're going for Por\/Spain\/Mun this turn?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":91214,"phase":"F1901M","message":"Interesting set of moves by Austria and Turkey. A prosperous Turkey does not sound good for Italy."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":91308,"phase":"F1901M","message":"Those A\/T orders should at least give you an argument for why Germany should not bounce you out of Sweden."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":91668,"phase":"F1901M","message":"Pretty strong set of orders. Are you still with me for F Alb-Ion? Turkey won't know we are springing a trap on him until after the spring orders and you retreat F Ion-Eastern Med. Can you get Turkey to support A Rum this fall in exchange for A Rum S Arm-Sev?"},{"sender":"ITALY","recipient":"GERMANY","time_sent":91706,"phase":"F1901M","message":"You have good information, and mine sucks, lol."},{"sender":"ITALY","recipient":"FRANCE","time_sent":91899,"phase":"F1901M","message":"Ok. This game is off to a rousing start."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":92141,"phase":"F1901M","message":"this AUS\/Turk allaince is going to be hard to beat. can you please allow me swe? i need the units in the ousth"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":93776,"phase":"F1901M","message":"Yes I will move my fleet in Albania into the Ionian this set, but I also have to worry about Russia in GAL"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":93792,"phase":"F1901M","message":"I thought we had a DMZ in Gal?"},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":93890,"phase":"F1901M","message":"Looks like Russia is making some aggressive moves our way."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":93958,"phase":"F1901M","message":"Can you explain that last sentence clearer? You want me to get Turkey to support hold Rum in exchange for Rum supporting a move into Sev?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":97121,"phase":"F1901M","message":"Ok, great. I will proceed with F Ion-Tunis.<br \/><br \/>Yes, it is a suggestion on a proposal you could make to Turkey to keep him from moving A Bulgaria to Greece or Serbia.<br \/><br \/>How do you feel about my moving to Tyrolia? I could use A Tyrolia next spring to support you if Russia takes Vienna. Plus it might have some deceptive value."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":97222,"phase":"F1901M","message":"Well, i may trust you... need a hand?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":97279,"phase":"F1901M","message":"Looking good for you as well!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":97369,"phase":"F1901M","message":"The move to Sev would have to be A Armenia-Sev."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":98740,"phase":"F1901M","message":"you took rum and are allied with turk. i had to premtive"},{"sender":"AUSTRIA","recipient":"TURKEY","time_sent":100645,"phase":"F1901M","message":"What can I do to help you?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":100765,"phase":"F1901M","message":"Yes you can have Sweden. There may be an English French alliance against me so I might need your support later."},{"sender":"GERMANY","recipient":"ITALY","time_sent":101244,"phase":"F1901M","message":"I'm not sure about the English French alliance, you could be right, we will see soon enough. There is a Turkish Austrian alliance against Russia. I am friendly with Russia so we might be able to form a 3 way alliance, at least it's good to have some friends to counter attack with."},{"sender":"ITALY","recipient":"GERMANY","time_sent":102936,"phase":"F1901M","message":"It's good for Russia your friendly. It looks like he is going to need Sweden."},{"sender":"ITALY","recipient":"GERMANY","time_sent":102971,"phase":"F1901M","message":"An alliance with Germany is something I would very much like."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":103846,"phase":"F1901M","message":"sounds good"},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":103848,"phase":"F1901M","message":"thanks"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":108963,"phase":"F1901M","message":"Can you move to Tyr? Im sure Russia is going to take Vie"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":109444,"phase":"F1901M","message":"Ok. Hopefully, Turkey will continue to attack Russia. Any gain he makes will be short term."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":117192,"phase":"F1901M","message":"I certainly wouldn't refuse the help."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":121474,"phase":"F1901M","message":"Not gonna talk?"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":132483,"phase":"F1901M","message":"I believe you will attack me."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":133924,"phase":"F1901M","message":"it's interesting because everyone of us is threatening everyone right now"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":149212,"phase":"F1901M","message":"I have nothing of yours to attack."}]},{"name":"F1901R","state":{"timestamp":1537459327380990,"zobrist_hash":"1587939958196966893","note":"","name":"F1901R","units":{"AUSTRIA":["A BUD","F ION","*A RUM"],"ENGLAND":["F NTH","F BRE","A NWY"],"FRANCE":["A BUR","A SPA","F POR"],"GERMANY":["F DEN","A RUH","A HOL"],"ITALY":["A TYR","F TUN","A VEN"],"RUSSIA":["F SEV","A GAL","A RUM","F SWE"],"TURKEY":["A ARM","F BLA","A SER"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","TRI","BUD","ALB","ION"],"ENGLAND":["EDI","LON","LVP","NTH","ENG","YOR","BRE","NWY"],"FRANCE":["MAR","PAR","MAO","BUR","SPA","POR"],"GERMANY":["BER","MUN","DEN","KIE","RUH","HOL"],"ITALY":["NAP","ROM","APU","TYR","TUN","VEN"],"RUSSIA":["WAR","MOS","SEV","STP","GAL","UKR","BOT","RUM","SWE"],"TURKEY":["ANK","CON","SMY","BUL","ARM","BLA","SER"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A RUM R BUL"],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A RUM":[]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":172708,"phase":"F1901R","message":"Good guess against A Gal. I think we have an RT alliance against us. Retreat to Bulgaria and build A Trieste. You need to build A Trieste verse A Vienna because you need to support Bul-Serbia next spring. You will probably get Bulgaria blown up in the spring unless you can make a supported attack on Trieste. We have a good plan to stymie them."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":172771,"phase":"F1901R","message":"Looks like an RT. Hopefully, you can sway Germany to the cause of kicking Russia out of Sweden."},{"sender":"ITALY","recipient":"GERMANY","time_sent":172913,"phase":"F1901R","message":"There appears to be a full-blown RT alliance. I expect A Arm-Bul next spring by convoy. This is really bad for Austria.<br \/><br \/>The RT is one of the most powerful alliances in Diplomacy. Once they eat Austria, we're next on their menu. The RT needs to be slowed down. Are you still willing to ally even if it means we have to fight Russia-Turkey together?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":173308,"phase":"F1901R","message":"My second to last sentence about A Bulgaria getting blown up wasn't clear. I meant you need to build A Trieste so you can make a supported attack (A Bulgaria-Serbia) using Army Trieste to support it. You need to use Budapest to cut support from Russia's A Rumania. That saves the power of your army now in Bulgaria for the fall turn."}]},{"name":"W1901A","state":{"timestamp":1537459327383090,"zobrist_hash":"1571834422959510240","note":"","name":"W1901A","units":{"AUSTRIA":["A BUD","F ION","A BUL"],"ENGLAND":["F NTH","F BRE","A NWY"],"FRANCE":["A BUR","A SPA","F POR"],"GERMANY":["F DEN","A RUH","A HOL"],"ITALY":["A TYR","F TUN","A VEN"],"RUSSIA":["F SEV","A GAL","A RUM","F SWE"],"TURKEY":["A ARM","F BLA","A SER"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","BUL"],"ENGLAND":["EDI","LON","LVP","BRE","NWY"],"FRANCE":["MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","SER"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","TRI","BUD","ALB","ION","BUL"],"ENGLAND":["EDI","LON","LVP","NTH","ENG","YOR","BRE","NWY"],"FRANCE":["MAR","PAR","MAO","BUR","SPA","POR"],"GERMANY":["BER","MUN","DEN","KIE","RUH","HOL"],"ITALY":["NAP","ROM","APU","TYR","TUN","VEN"],"RUSSIA":["WAR","MOS","SEV","STP","GAL","UKR","BOT","RUM","SWE"],"TURKEY":["ANK","CON","SMY","ARM","BLA","SER"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":1,"homes":["TRI","VIE"]},"ENGLAND":{"count":2,"homes":["EDI","LON","LVP"]},"FRANCE":{"count":1,"homes":["MAR","PAR"]},"GERMANY":{"count":2,"homes":["BER","KIE","MUN"]},"ITALY":{"count":1,"homes":["NAP","ROM"]},"RUSSIA":{"count":2,"homes":["MOS","STP","WAR"]},"TURKEY":{"count":1,"homes":["ANK","CON","SMY"]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A TRI B"],"ENGLAND":["F EDI B","F LON B"],"FRANCE":["A PAR B"],"GERMANY":["F KIE B","A MUN B"],"ITALY":["F NAP B"],"RUSSIA":["A MOS B","A STP B"],"TURKEY":["A CON B"]},"results":{"A TRI":[""],"F EDI":[""],"F LON":[""],"A PAR":[""],"F KIE":[""],"A MUN":[""],"F NAP":[""],"A MOS":[""],"A STP":[""],"A CON":[""]},"messages":[{"sender":"GERMANY","recipient":"ITALY","time_sent":175050,"phase":"W1901A","message":"I don't think there is a Russian Turkish alliance. Austria made some bizarre moves. You can build a fleet in Naples and take Trieste. I am with you whatever happens, well unless you stab me.. lol<br \/>There are often people who cheat and play two countries, multi accounts, we will have to see what Austria and Turkey do."},{"sender":"ITALY","recipient":"GERMANY","time_sent":175396,"phase":"W1901A","message":"Yeah, that was a new Austrian opening for me."},{"sender":"ITALY","recipient":"GERMANY","time_sent":175529,"phase":"W1901A","message":"I think I'm better off trying to prop Austria up as best I can until we know for sure we are not dealing with an RT."},{"sender":"ITALY","recipient":"GERMANY","time_sent":175671,"phase":"W1901A","message":"The thing is why didn't Turkey order A Arm-Sev if he was really hostile to Russia?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":182215,"phase":"W1901A","message":"Yeah, that was dumb of Germany. I've tried messaging them but for some reason they think I'm against them."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":182257,"phase":"W1901A","message":"Hopefully you see now that (a) you should have bounced Russia in Sweden and (b) I'm not against you. Will you help me kick Russia out of Sweden with support from Denmark?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":182718,"phase":"W1901A","message":"Seems like your taking Brest would be convincing."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":185522,"phase":"W1901A","message":"Let's hope so. Have you tried talking to Germany?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":191041,"phase":"W1901A","message":"Yes. He is skeptical of an RT."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":217238,"phase":"W1901A","message":"What would I get in exchange?"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":236021,"phase":"W1901A","message":"I fail to understand your communication strategy here. Would to like to work together here at all, or no? France and Italy have move on Munich so I'd think you'd want an ally. You help me, I help you. What would you want?"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":238092,"phase":"W1901A","message":"Exactly, you have nothing to offer."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":239263,"phase":"W1901A","message":"Ok then!"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":239296,"phase":"W1901A","message":"Germany being dumb and not talking. So count them out and consider moving on Munich. Annoying player to deal with, at least so far."}]},{"name":"S1902M","state":{"timestamp":1537459327393605,"zobrist_hash":"995136048432796641","note":"","name":"S1902M","units":{"AUSTRIA":["A BUD","F ION","A BUL","A TRI"],"ENGLAND":["F NTH","F BRE","A NWY","F EDI","F LON"],"FRANCE":["A BUR","A SPA","F POR","A PAR"],"GERMANY":["F DEN","A RUH","A HOL","F KIE","A MUN"],"ITALY":["A TYR","F TUN","A VEN","F NAP"],"RUSSIA":["F SEV","A GAL","A RUM","F SWE","A MOS","A STP"],"TURKEY":["A ARM","F BLA","A SER","A CON"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","BUL"],"ENGLAND":["EDI","LON","LVP","BRE","NWY"],"FRANCE":["MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","SER"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","TRI","BUD","ALB","ION","BUL"],"ENGLAND":["EDI","LON","LVP","NTH","ENG","YOR","BRE","NWY"],"FRANCE":["MAR","PAR","MAO","BUR","SPA","POR"],"GERMANY":["BER","MUN","DEN","KIE","RUH","HOL"],"ITALY":["NAP","ROM","APU","TYR","TUN","VEN"],"RUSSIA":["WAR","MOS","SEV","STP","GAL","UKR","BOT","RUM","SWE"],"TURKEY":["ANK","CON","SMY","ARM","BLA","SER"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD H","F ION H","A BUL H","A TRI H"],"ENGLAND":["F NTH S A NWY","A NWY H","F BRE H","F EDI - NWG","F LON - ENG"],"FRANCE":["A BUR H","A SPA - GAS","F POR - SPA\/NC","A PAR - BRE"],"GERMANY":["F DEN H","A RUH S A HOL - BEL","A HOL - BEL","F KIE H","A MUN H"],"ITALY":["A TYR H","A VEN - APU","F TUN S F NAP - ION","F NAP - ION"],"RUSSIA":["F SEV S A RUM","A GAL - BUD","A RUM S A GAL - BUD","F SWE S A STP - NWY","A MOS S F SEV","A STP - NWY"],"TURKEY":["A ARM - BUL VIA","A SER - GRE","F BLA C A ARM - BUL","A CON S A ARM - BUL"]},"results":{"A BUD":["dislodged"],"F ION":["dislodged"],"A BUL":["dislodged"],"A TRI":[],"F NTH":[],"F BRE":[],"A NWY":[],"F EDI":[],"F LON":[],"A BUR":[],"A SPA":[],"F POR":[],"A PAR":["bounce"],"F DEN":[],"A RUH":[],"A HOL":[],"F KIE":[],"A MUN":[],"A TYR":[],"F TUN":[],"A VEN":[],"F NAP":[],"F SEV":[],"A GAL":[],"A RUM":[],"F SWE":[],"A MOS":[],"A STP":["bounce"],"A ARM":[],"F BLA":[],"A SER":[],"A CON":[]},"messages":[]},{"name":"S1902R","state":{"timestamp":1537459327395862,"zobrist_hash":"7653646099187272822","note":"","name":"S1902R","units":{"AUSTRIA":["A TRI","*A BUD","*F ION","*A BUL"],"ENGLAND":["F NTH","F BRE","A NWY","F NWG","F ENG"],"FRANCE":["A BUR","A PAR","A GAS","F SPA\/NC"],"GERMANY":["F DEN","A RUH","F KIE","A MUN","A BEL"],"ITALY":["A TYR","F TUN","A APU","F ION"],"RUSSIA":["F SEV","A RUM","F SWE","A MOS","A STP","A BUD"],"TURKEY":["F BLA","A CON","A BUL","A GRE"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","BUL"],"ENGLAND":["EDI","LON","LVP","BRE","NWY"],"FRANCE":["MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","SER"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","TRI","ALB"],"ENGLAND":["EDI","LON","LVP","NTH","YOR","BRE","NWY","NWG","ENG"],"FRANCE":["MAR","PAR","MAO","BUR","POR","GAS","SPA"],"GERMANY":["BER","MUN","DEN","KIE","RUH","HOL","BEL"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION"],"RUSSIA":["WAR","MOS","SEV","STP","GAL","UKR","BOT","RUM","SWE","BUD"],"TURKEY":["ANK","CON","SMY","ARM","BLA","SER","BUL","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD R VIE","A BUL R SER","F ION R AEG"],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A BUD":[],"F ION":[],"A BUL":[]},"messages":[]},{"name":"F1902M","state":{"timestamp":1537459327407187,"zobrist_hash":"7829426377686728481","note":"","name":"F1902M","units":{"AUSTRIA":["A TRI","A VIE","A SER","F AEG"],"ENGLAND":["F NTH","F BRE","A NWY","F NWG","F ENG"],"FRANCE":["A BUR","A PAR","A GAS","F SPA\/NC"],"GERMANY":["F DEN","A RUH","F KIE","A MUN","A BEL"],"ITALY":["A TYR","F TUN","A APU","F ION"],"RUSSIA":["F SEV","A RUM","F SWE","A MOS","A STP","A BUD"],"TURKEY":["F BLA","A CON","A BUL","A GRE"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","BUL"],"ENGLAND":["EDI","LON","LVP","BRE","NWY"],"FRANCE":["MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","SER"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI","ALB","VIE","SER","AEG"],"ENGLAND":["EDI","LON","LVP","NTH","YOR","BRE","NWY","NWG","ENG"],"FRANCE":["MAR","PAR","MAO","BUR","POR","GAS","SPA"],"GERMANY":["BER","MUN","DEN","KIE","RUH","HOL","BEL"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION"],"RUSSIA":["WAR","MOS","SEV","STP","GAL","UKR","BOT","RUM","SWE","BUD"],"TURKEY":["ANK","CON","SMY","ARM","BLA","BUL","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A TRI S A SER - BUD","A SER - BUD","F AEG - SMY","A VIE S A SER - BUD"],"ENGLAND":["F NTH S F ENG - BEL","A NWY H","F BRE - MAO","F NWG S A NWY","F ENG - BEL"],"FRANCE":["A BUR - BEL","A PAR S A GAS - BRE","F SPA\/NC - MAO","A GAS - BRE"],"GERMANY":["F DEN H","A RUH S A BEL","F KIE - HOL","A MUN H","A BEL H"],"ITALY":["A TYR - VEN","F TUN - ION","F ION - EAS","A APU - VEN"],"RUSSIA":["F SEV - RUM","A RUM S A BUD","F SWE S A STP - NWY","A MOS - SEV","A STP - NWY","A BUD S F SEV - RUM"],"TURKEY":["F BLA - CON","A CON - SMY","A GRE S A BUL","A BUL S A GRE"]},"results":{"A TRI":[],"A VIE":[],"A SER":[],"F AEG":["bounce"],"F NTH":[],"F BRE":["bounce","dislodged"],"A NWY":[],"F NWG":[],"F ENG":["bounce"],"A BUR":["bounce"],"A PAR":[],"A GAS":[],"F SPA\/NC":["bounce"],"F DEN":[],"A RUH":[],"F KIE":[],"A MUN":[],"A BEL":[],"A TYR":["bounce"],"F TUN":[],"A APU":["bounce"],"F ION":[],"F SEV":["bounce"],"A RUM":[],"F SWE":[],"A MOS":["bounce"],"A STP":["bounce"],"A BUD":["cut","dislodged"],"F BLA":["bounce"],"A CON":["bounce"],"A BUL":[],"A GRE":[]},"messages":[{"sender":"AUSTRIA","recipient":"ITALY","time_sent":327785,"phase":"F1902M","message":"Now what?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":334447,"phase":"F1902M","message":"So glad you didn't nmr your retreats!<br \/><br \/>My guess is Russia will support his army in budapest with Rumania, and A Budapest will not move. I recommend these orders:<br \/><br \/>A Vienna-Budapest (to cut support for a possible Turkish move to Serbia);<br \/>A Trieste S A Serbia H;<br \/>A Serbia S ITALIAN F Ionian-Greece;<br \/>F Aegean S ITALIAN F Ionian-Greece.<br \/><br \/>We need to target Turkey with all our offensive power. Maybe later we can talk Russia into moving against Turkey if we can weaken the Sultan enough and throw up some roadblocks.<br \/><br \/>Germany is actively helping the Russian against England, but that looks like a stalemate. It should give us time to get you back to growing, hopefully."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":334622,"phase":"F1902M","message":"Plus, Turkey made a mistake by not building a fleet. We will go on a naval offensive next Spring."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":336503,"phase":"F1902M","message":"looks like i can trust you now - you want to cord against aus?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":338003,"phase":"F1902M","message":"Are you still interested in moving on Germany? For some reason they don't want to help me against France, so I might as well join you against them. Will you agree to leave Norway and Sweden as they are?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":339650,"phase":"F1902M","message":"Nmr?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":341609,"phase":"F1902M","message":"Yeah sorry, I was on a trip but I'm good to go now."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":342145,"phase":"F1902M","message":"Let me think about this some more. You need to stay at 4 and not disband."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":346323,"phase":"F1902M","message":"I think turkey will move f bla con and a con to smy. He has to get a fleet into the med. therefore, these are your boldest moves.<br \/><br \/>A Ser-Bul <br \/>F Aeg s Ser to Bul<br \/>A Tri - Ser.<br \/><br \/>I will try a convoy to gre to cut support for A Bul.<br \/><br \/>Thoughts?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":346425,"phase":"F1902M","message":"Crap I just sent you a message on global. Best to stick with the first plan."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":346477,"phase":"F1902M","message":"This is misinformation."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":351605,"phase":"F1902M","message":"Ok"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":356460,"phase":"F1902M","message":"What do you think of this:<br \/><br \/>A Serbia-Budapest; A Vienna S Serbia-Budapest; A Trieste S Serbia-Budapest; F Aeg-Smyrna.<br \/><br \/>Gives you a shot at two. Just don't disband your fleet."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":361594,"phase":"F1902M","message":"Haha gold"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":361670,"phase":"F1902M","message":"Yeah that's best. I won't disband the fleet"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":364690,"phase":"F1902M","message":"What do you think of this:<br \/><br \/>A Serbia-Budapest; A Vienna S Serbia-Budapest; A Trieste S Serbia-Budapest; F Aeg-Smyrna.<br \/><br \/>Gives you a shot at two. Just don't disband your fleet."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":364725,"phase":"F1902M","message":"Sorry, double post."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":364837,"phase":"F1902M","message":"I will continue trying to warn him of what an RT does to Germany."},{"sender":"ITALY","recipient":"GERMANY","time_sent":364958,"phase":"F1902M","message":"Yeah, the convoy order happened. I think the Russian Turkish alliance is confirmed."}]},{"name":"F1902R","state":{"timestamp":1537459327409271,"zobrist_hash":"8332112481556014387","note":"","name":"F1902R","units":{"AUSTRIA":["A TRI","A VIE","F AEG","A BUD"],"ENGLAND":["F NTH","A NWY","F NWG","F ENG","*F BRE"],"FRANCE":["A BUR","A PAR","F SPA\/NC","A BRE"],"GERMANY":["F DEN","A RUH","A MUN","A BEL","F HOL"],"ITALY":["A TYR","A APU","F ION","F EAS"],"RUSSIA":["F SEV","A RUM","F SWE","A MOS","A STP","*A BUD"],"TURKEY":["F BLA","A CON","A BUL","A GRE"]},"centers":{"AUSTRIA":["BUD","TRI","VIE","BUL"],"ENGLAND":["EDI","LON","LVP","BRE","NWY"],"FRANCE":["MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","SER"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI","ALB","VIE","SER","AEG","BUD"],"ENGLAND":["EDI","LON","LVP","NTH","YOR","NWY","NWG","ENG"],"FRANCE":["MAR","PAR","MAO","BUR","POR","GAS","SPA","BRE"],"GERMANY":["BER","MUN","DEN","KIE","RUH","BEL","HOL"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION","EAS"],"RUSSIA":["WAR","MOS","SEV","STP","GAL","UKR","BOT","RUM","SWE"],"TURKEY":["ANK","CON","SMY","ARM","BLA","BUL","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F BRE R PIC"],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUD R GAL"],"TURKEY":[]},"results":{"F BRE":[],"A BUD":[]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":414355,"phase":"F1902R","message":"Can we switch up and take down Turkey now? You and I would make great allies at this point. But I need to kill Turkey. After that, Austria, who is weak and will be between our armies."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":414400,"phase":"F1902R","message":"We are just going to stalemate each other if we keep this up. Long term, that leads to death for me (at French hands)."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":414751,"phase":"F1902R","message":"When it's time, disband A Vienna and we can try to defend it with the help of A Tyo. With luck, we are both going to take a center off Turkey this year (Greece and Smyrna). Hopefully, we can get Russia to go north and take Nwy."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":414753,"phase":"F1902R","message":"Ok. Things could be worse."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":414765,"phase":"F1902R","message":"I'm not going to stab you. Relax"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":414775,"phase":"F1902R","message":"Re: your bounce haha"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":414792,"phase":"F1902R","message":"Plus, you might get Serbia if Russia alleviates the pressure and\/or doesn't help Turkey anymore."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":414830,"phase":"F1902R","message":"I didn't have any other moves for those two units, so why not?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":415025,"phase":"F1902R","message":"Yeah"},{"sender":"FRANCE","recipient":"ITALY","time_sent":415712,"phase":"F1902R","message":"starts looking better for the both of us again..."}]},{"name":"W1902A","state":{"timestamp":1537459327411166,"zobrist_hash":"1050431836970149434","note":"","name":"W1902A","units":{"AUSTRIA":["A TRI","A VIE","F AEG","A BUD"],"ENGLAND":["F NTH","A NWY","F NWG","F ENG","F PIC"],"FRANCE":["A BUR","A PAR","F SPA\/NC","A BRE"],"GERMANY":["F DEN","A RUH","A MUN","A BEL","F HOL"],"ITALY":["A TYR","A APU","F ION","F EAS"],"RUSSIA":["F SEV","A RUM","F SWE","A MOS","A STP","A GAL"],"TURKEY":["F BLA","A CON","A BUL","A GRE"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","SER","BUL","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI","ALB","VIE","SER","AEG","BUD"],"ENGLAND":["EDI","LON","LVP","NTH","YOR","NWY","NWG","ENG","PIC"],"FRANCE":["MAR","PAR","MAO","BUR","POR","GAS","SPA","BRE"],"GERMANY":["BER","MUN","DEN","KIE","RUH","BEL","HOL"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION","EAS"],"RUSSIA":["WAR","MOS","SEV","STP","UKR","BOT","RUM","SWE","GAL"],"TURKEY":["ANK","CON","SMY","ARM","BLA","BUL","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":-1,"homes":[]},"ENGLAND":{"count":-1,"homes":[]},"FRANCE":{"count":1,"homes":["MAR"]},"GERMANY":{"count":1,"homes":["BER","KIE"]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":2,"homes":["ANK","SMY"]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A TRI D"],"ENGLAND":["A NWY D"],"FRANCE":["A MAR B"],"GERMANY":["F KIE B"],"ITALY":[],"RUSSIA":[],"TURKEY":["A ANK B","F SMY B"]},"results":{"A TRI":[""],"A NWY":[""],"A MAR":[""],"F KIE":[""],"A ANK":[""],"F SMY":[""]},"messages":[{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":419740,"phase":"W1902A","message":"i dont like your army in norway. if you replace it with a fleet, ill be open to it"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":420405,"phase":"W1902A","message":"Hmm. Part of me wants to destroy the Agean fleet. You seem have control down there, and I I feel my army up north could be better suited fending off Germany\/Russia"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":420412,"phase":"W1902A","message":"What do you think?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":422392,"phase":"W1902A","message":"I can't move it without you stopping your support move."},{"sender":"ITALY","recipient":"FRANCE","time_sent":424226,"phase":"W1902A","message":"Congratulations. Looks like England is a bit of hot water. I predict F Kiel is in the works."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":424500,"phase":"W1902A","message":"Definitely don't pick up the fleet. We will be totally stymied against Turkey without it. <br \/><br \/>You should pick up either Vienna or Trieste. I have A Tyrolia to help you in the north. Budapest-Serbia supported by Trieste. If Russia tries to take Bud again, you will be able to reteat to Vienna. If he takes Vienna, we can kick him out again with my support. <br \/><br \/>Strategically, we need to hammer the Turk this year and we need F Aegean for that work."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":424630,"phase":"W1902A","message":"Also, F Aegean can provide you uncuttable support for moves against Greece or Smyrna. It is extremely valuable and forces Turkey into defense."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":424877,"phase":"W1902A","message":"What if Austria supported A Rum-Serbia? You move Gal-Rum S by F Sev, and then put A Moscow to use in the north by taking Finland and moving to Stp."},{"sender":"FRANCE","recipient":"ITALY","time_sent":426820,"phase":"W1902A","message":"i agree. only hope that england will still blockade germany for a bit longer"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":430689,"phase":"W1902A","message":"Destroying at Trieste"}]},{"name":"S1903M","state":{"timestamp":1537459327423677,"zobrist_hash":"4636916645626547987","note":"","name":"S1903M","units":{"AUSTRIA":["A VIE","F AEG","A BUD"],"ENGLAND":["F NTH","F NWG","F ENG","F PIC"],"FRANCE":["A BUR","A PAR","F SPA\/NC","A BRE","A MAR"],"GERMANY":["F DEN","A RUH","A MUN","A BEL","F HOL","F KIE"],"ITALY":["A TYR","A APU","F ION","F EAS"],"RUSSIA":["F SEV","A RUM","F SWE","A MOS","A STP","A GAL"],"TURKEY":["F BLA","A CON","A BUL","A GRE","A ANK","F SMY"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","SER","BUL","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI","ALB","VIE","SER","AEG","BUD"],"ENGLAND":["EDI","LON","LVP","NTH","YOR","NWY","NWG","ENG","PIC"],"FRANCE":["MAR","PAR","MAO","BUR","POR","GAS","SPA","BRE"],"GERMANY":["BER","MUN","DEN","KIE","RUH","BEL","HOL"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION","EAS"],"RUSSIA":["WAR","MOS","SEV","STP","UKR","BOT","RUM","SWE","GAL"],"TURKEY":["ANK","CON","SMY","ARM","BLA","BUL","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["F AEG - GRE","A VIE - BUD","A BUD - SER"],"ENGLAND":["F NTH S F PIC - BEL","F NWG - NWY","F ENG S F PIC - BEL","F PIC - BEL"],"FRANCE":["A BUR S F PIC - BEL","A PAR S A BRE","F SPA\/NC - MAO","A BRE H","A MAR - GAS"],"GERMANY":["F DEN - NTH","A RUH - BUR","A MUN S A RUH - BUR","A BEL S A RUH - BUR","F HOL S F DEN - NTH","F KIE - HEL"],"ITALY":["A TYR - TRI","A APU - ALB VIA","F ION C A APU - ALB","F EAS - SMY"],"RUSSIA":["F SEV - ARM","A RUM S A GAL - BUD","F SWE S A STP - NWY","A MOS - SEV","A STP - NWY","A GAL - BUD"],"TURKEY":["F BLA - CON","A CON - BUL","A GRE S A BUL - SER","A BUL - SER","F SMY - AEG","A ANK - SMY"]},"results":{"A VIE":["bounce"],"F AEG":["bounce"],"A BUD":["bounce","dislodged"],"F NTH":["cut","dislodged"],"F NWG":["bounce"],"F ENG":[],"F PIC":[],"A BUR":["cut","dislodged"],"A PAR":[],"F SPA\/NC":[],"A BRE":[],"A MAR":[],"F DEN":[],"A RUH":[],"A MUN":[],"A BEL":["cut","dislodged"],"F HOL":[],"F KIE":[],"A TYR":[],"A APU":[],"F ION":[],"F EAS":["bounce"],"F SEV":[],"A RUM":[],"F SWE":[],"A MOS":[],"A STP":[],"A GAL":[],"F BLA":["bounce"],"A CON":["bounce"],"A BUL":["bounce"],"A GRE":["cut"],"A ANK":["bounce"],"F SMY":["bounce"]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":434670,"phase":"S1903M","message":"What do you think about A Tyr-Trieste in the spring, and then A Trieste-Serbia in the fall? <br \/><br \/>You would need to order A Vienna-Gal; A Bud-Serbia; F Aeg-Bul. Budapest would have a retreat to Vienna if the moves don't work out, and I'll convoy to Albania for a supported attack on Serbia with the two Italian armies in the fall. I will support your F Aeg-Gre in the fall with F Ion.<br \/><br \/>Alternatively, I can convoy to Syria, but that doesn't put my armies into much of a position to help you in the Balkans, and Turkey's going to be a tough nut to crack until we can force a disband or two."},{"sender":"ITALY","recipient":"FRANCE","time_sent":434756,"phase":"S1903M","message":"Based on the disbands, it looks like he wants to throw in with you against Germany."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":434809,"phase":"S1903M","message":"I am a fan of that move. I've done it as Italy in the past on the 1st turn, just sprinting to Serbia."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":434944,"phase":"S1903M","message":"Ok, let's try it. Our situation is getting more desperate."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":436451,"phase":"S1903M","message":"how about we forget our starting difficulties and join forces against the Germans?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":439683,"phase":"S1903M","message":"Maybe Aeg-Gre is a better move. I bet A Gre throws support for Bul-Ser. You need to guess right to cut support for the move to Serbia. If we can keep Turkey all bottled up in Turkey it will help us get Serbia and Greece."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":446000,"phase":"S1903M","message":"Help me into Belgium then?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":449150,"phase":"S1903M","message":"yes, alright"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":452722,"phase":"S1903M","message":"OK, from Pic."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":465751,"phase":"S1903M","message":"wait im a little confused on that"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":465804,"phase":"S1903M","message":"any ideas? lets dmz arm too"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":482087,"phase":"S1903M","message":"good, that was what i thought"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":492019,"phase":"S1903M","message":"Ok for dmz black sea and armenia"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":511915,"phase":"S1903M","message":"I want to ally with you, but I cannot because you're allied with Turkey. Turkey is a strategic threat to Italy's existence. I would like you to ditch Turkey and work with Italy."},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":516322,"phase":"S1903M","message":"youll leave BS? how"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":516341,"phase":"S1903M","message":"i want to work with you - im not allied with turkey - he never talks to me"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":516365,"phase":"S1903M","message":"thats why i tried to position my units around BS better last turn"}]},{"name":"S1903R","state":{"timestamp":1537459327426655,"zobrist_hash":"8713666109675045950","note":"","name":"S1903R","units":{"AUSTRIA":["A VIE","F AEG"],"ENGLAND":["F NWG","F ENG","F BEL","*F NTH"],"FRANCE":["A PAR","A BRE","F MAO","A GAS","*A BUR"],"GERMANY":["A MUN","F HOL","F NTH","A BUR","F HEL","*A BEL"],"ITALY":["F ION","F EAS","A TRI","A ALB"],"RUSSIA":["A RUM","F SWE","F ARM","A SEV","A NWY","A BUD"],"TURKEY":["F BLA","A CON","A BUL","A GRE","A ANK","F SMY"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","SER","BUL","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","SER","AEG"],"ENGLAND":["EDI","LON","LVP","YOR","NWG","ENG","PIC","BEL"],"FRANCE":["MAR","PAR","POR","SPA","BRE","MAO","GAS"],"GERMANY":["BER","MUN","DEN","KIE","RUH","HOL","NTH","BUR","HEL"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION","EAS","TRI","ALB"],"RUSSIA":["WAR","MOS","STP","UKR","BOT","RUM","SWE","GAL","ARM","SEV","NWY","BUD"],"TURKEY":["ANK","CON","SMY","BLA","BUL","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NTH R EDI"],"FRANCE":["A BUR R MAR"],"GERMANY":["A BEL R RUH"],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"F NTH":[],"A BUR":[],"A BEL":[],"A BUD":["disband"]},"messages":[{"sender":"AUSTRIA","recipient":"ITALY","time_sent":519321,"phase":"S1903R","message":"Well Italy, I screwed this game up royally. I will do what I can to help you, but I think it's clear that your future is beyond my own."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":523074,"phase":"S1903R","message":"I believe it. He hasn't sent me one message all game. <br \/><br \/>We are in pretty good shape for an alliance, which I want. Let's ally, and then kill Turkey and Austria."}]},{"name":"F1903M","state":{"timestamp":1537459327440036,"zobrist_hash":"2594943441397720633","note":"","name":"F1903M","units":{"AUSTRIA":["A VIE","F AEG"],"ENGLAND":["F NWG","F ENG","F BEL","F EDI"],"FRANCE":["A PAR","A BRE","F MAO","A GAS","A MAR"],"GERMANY":["A MUN","F HOL","F NTH","A BUR","F HEL","A RUH"],"ITALY":["F ION","F EAS","A TRI","A ALB"],"RUSSIA":["A RUM","F SWE","F ARM","A SEV","A NWY","A BUD"],"TURKEY":["F BLA","A CON","A BUL","A GRE","A ANK","F SMY"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","SER","BUL","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","SER","AEG"],"ENGLAND":["LON","LVP","YOR","NWG","ENG","PIC","BEL","EDI"],"FRANCE":["PAR","POR","SPA","BRE","MAO","GAS","MAR"],"GERMANY":["BER","MUN","DEN","KIE","HOL","NTH","BUR","HEL","RUH"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION","EAS","TRI","ALB"],"RUSSIA":["WAR","MOS","STP","UKR","BOT","RUM","SWE","GAL","ARM","SEV","NWY","BUD"],"TURKEY":["ANK","CON","SMY","BLA","BUL","GRE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["F AEG - GRE","A VIE - BUD"],"ENGLAND":["F NWG - NTH","F ENG - LON","F BEL S F NWG - NTH","F EDI S F NWG - NTH"],"FRANCE":["A PAR S A MAR - BUR","A BRE H","A GAS S A MAR - BUR","F MAO - ENG","A MAR - BUR"],"GERMANY":["A MUN H","F HOL S F NTH","F NTH - LON","F HEL - DEN","A BUR - BEL","A RUH S A BUR - BEL"],"ITALY":["F ION S F AEG - GRE","F EAS - SMY","A ALB S A TRI - SER","A TRI - SER"],"RUSSIA":["A RUM - BUL","F SWE S A NWY","F ARM - ANK","A SEV - RUM","A NWY S F SWE","A BUD - RUM"],"TURKEY":["F BLA S A ANK - ARM","A CON - SMY","A GRE H","A BUL S A GRE","F SMY - AEG","A ANK - ARM"]},"results":{"A VIE":["bounce"],"F AEG":[],"F NWG":[],"F ENG":["bounce"],"F BEL":["cut","dislodged"],"F EDI":[],"A PAR":[],"A BRE":[],"F MAO":["bounce"],"A GAS":[],"A MAR":[],"A MUN":[],"F HOL":["void"],"F NTH":["bounce","dislodged"],"A BUR":[],"F HEL":[],"A RUH":[],"F ION":[],"F EAS":["bounce"],"A TRI":[],"A ALB":[],"A RUM":["bounce"],"F SWE":[],"F ARM":["bounce","dislodged"],"A SEV":["bounce"],"A NWY":[],"A BUD":["bounce"],"F BLA":[],"A CON":["bounce"],"A BUL":["cut"],"A GRE":["dislodged"],"A ANK":[],"F SMY":[]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":523417,"phase":"F1903M","message":"Ya, in hindsight the opening Bud to Rumania might have made things more difficult than they needed to be. Still, while it seems bad, I think you might survive the year with three units. I am supporting F Aeg-Greece, and using A Trieste to support A Trieste to Serbia."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":523531,"phase":"F1903M","message":"If you agree, A Bud-Rum; A Sev S Bud-Rum; and A Rum-Bul; mean maximum damage to both of them right now!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":523721,"phase":"F1903M","message":"Oh, I see, you didn't move A Vie to Galacia. Shit!"},{"sender":"ITALY","recipient":"GERMANY","time_sent":524021,"phase":"F1903M","message":"Yay! There is no Russian Turkish alliance!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":524492,"phase":"F1903M","message":"If you get Greece and I Serbia, you will be able to build A Trieste. Keep your fingers crossed!"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":542735,"phase":"F1903M","message":"I have ordered A Trieste-Serbia; A Albania S Trieste-Ser; and F Ion S AUSTRIAN F Aeg-Greece.<br \/><br \/>Please also order A Vienna to Budapest to cut Russian support, or to disrupt an aggressive Russian attempt to get two of your dots by A Bud-Tri; A Rum-Bud."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":542767,"phase":"F1903M","message":"Done"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":548938,"phase":"F1903M","message":"why hit bul? i cant get in and i want to ensure he doesnt displace me in rum"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":568750,"phase":"F1903M","message":"if you want to protect london should i enter the channel?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":580784,"phase":"F1903M","message":"The offensive reason is to cut Turkish support for Greece or Serbia ensuring he loses two centers this winter. If you order A Bud-Rum; A Sev S Bud-Rum; you protect Rumania from the supported attack. Plus, your unit in Bud will bounce because it's against the rules to dislodge one of your own units. It's the beleaguered unit rule. It allows you to move Rumania without risk."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":586617,"phase":"F1903M","message":"Looks like Russia might have flipped again. Maybe you can convince France to work with you. With Russia building F Stp, that's a lot of sea power in the hands of G\/r. France should be worried about Germany taking your dots. She is next."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":586695,"phase":"F1903M","message":"Belgium looks like a lost cause. Perhaps F Bel-Nth; F Edi S Bel-Nth; F Eng-London; and pray you have a French ally who will move against Germany in the lowlands."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":588426,"phase":"F1903M","message":"Go ahead"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":592076,"phase":"F1903M","message":"good call"}]},{"name":"F1903R","state":{"timestamp":1537459327442531,"zobrist_hash":"3632042740045106435","note":"","name":"F1903R","units":{"AUSTRIA":["A VIE","F GRE"],"ENGLAND":["F ENG","F EDI","F NTH","*F BEL"],"FRANCE":["A PAR","A BRE","F MAO","A GAS","A BUR"],"GERMANY":["A MUN","F HOL","A RUH","A BEL","F DEN","*F NTH"],"ITALY":["F ION","F EAS","A ALB","A SER"],"RUSSIA":["A RUM","F SWE","A SEV","A NWY","A BUD"],"TURKEY":["F BLA","A CON","A BUL","A ARM","F AEG"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE"],"TURKEY":["ANK","CON","SMY","SER","BUL","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","GRE"],"ENGLAND":["LON","LVP","YOR","NWG","ENG","PIC","EDI","NTH"],"FRANCE":["PAR","POR","SPA","BRE","MAO","GAS","MAR","BUR"],"GERMANY":["BER","MUN","KIE","HOL","HEL","RUH","BEL","DEN"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION","EAS","TRI","ALB","SER"],"RUSSIA":["WAR","MOS","STP","UKR","BOT","RUM","SWE","GAL","SEV","NWY","BUD"],"TURKEY":["ANK","CON","SMY","BLA","BUL","ARM","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F BEL D"],"FRANCE":[],"GERMANY":["F NTH R SKA"],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"F BEL":["disband"],"F NTH":[],"A GRE":["disband"],"F ARM":["disband"]},"messages":[]},{"name":"W1903A","state":{"timestamp":1537459327444400,"zobrist_hash":"3621361829227255584","note":"","name":"W1903A","units":{"AUSTRIA":["A VIE","F GRE"],"ENGLAND":["F ENG","F EDI","F NTH"],"FRANCE":["A PAR","A BRE","F MAO","A GAS","A BUR"],"GERMANY":["A MUN","F HOL","A RUH","A BEL","F DEN","F SKA"],"ITALY":["F ION","F EAS","A ALB","A SER"],"RUSSIA":["A RUM","F SWE","A SEV","A NWY","A BUD"],"TURKEY":["F BLA","A CON","A BUL","A ARM","F AEG"]},"centers":{"AUSTRIA":["TRI","VIE","GRE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["MAR","PAR","POR","SPA","BRE"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","BUD","NWY"],"TURKEY":["ANK","CON","SMY","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","GRE"],"ENGLAND":["LON","LVP","YOR","NWG","ENG","PIC","EDI","NTH"],"FRANCE":["PAR","POR","SPA","BRE","MAO","GAS","MAR","BUR"],"GERMANY":["BER","MUN","KIE","HOL","HEL","RUH","BEL","DEN","SKA"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION","EAS","TRI","ALB","SER"],"RUSSIA":["WAR","MOS","STP","UKR","BOT","RUM","SWE","GAL","SEV","NWY","BUD"],"TURKEY":["ANK","CON","SMY","BLA","BUL","ARM","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":1,"homes":["TRI"]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":1,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":3,"homes":["MOS","STP","WAR"]},"TURKEY":{"count":-1,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A TRI B"],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":["A VEN B"],"RUSSIA":["A MOS B","F STP\/NC B","A WAR B"],"TURKEY":["F BLA D"]},"results":{"A TRI":[""],"A VEN":[""],"A MOS":[""],"F STP\/NC":[""],"A WAR":[""],"F BLA":[""]},"messages":[]},{"name":"S1904M","state":{"timestamp":1537459327457295,"zobrist_hash":"4201809128096090279","note":"","name":"S1904M","units":{"AUSTRIA":["A VIE","F GRE","A TRI"],"ENGLAND":["F ENG","F EDI","F NTH"],"FRANCE":["A PAR","A BRE","F MAO","A GAS","A BUR"],"GERMANY":["A MUN","F HOL","A RUH","A BEL","F DEN","F SKA"],"ITALY":["F ION","F EAS","A ALB","A SER","A VEN"],"RUSSIA":["A RUM","F SWE","A SEV","A NWY","A BUD","A MOS","F STP\/NC","A WAR"],"TURKEY":["A CON","A BUL","A ARM","F AEG"]},"centers":{"AUSTRIA":["TRI","VIE","GRE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["MAR","PAR","POR","SPA","BRE"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","BUD","NWY"],"TURKEY":["ANK","CON","SMY","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","GRE"],"ENGLAND":["LON","LVP","YOR","NWG","ENG","PIC","EDI","NTH"],"FRANCE":["PAR","POR","SPA","BRE","MAO","GAS","MAR","BUR"],"GERMANY":["BER","MUN","KIE","HOL","HEL","RUH","BEL","DEN","SKA"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION","EAS","TRI","ALB","SER"],"RUSSIA":["WAR","MOS","STP","UKR","BOT","RUM","SWE","GAL","SEV","NWY","BUD"],"TURKEY":["ANK","CON","SMY","BLA","BUL","ARM","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE - GAL","F GRE - AEG","A TRI - BUD"],"ENGLAND":["F ENG S F NTH","F EDI S F NTH","F NTH H"],"FRANCE":["A PAR S A GAS - BUR","A BRE S A BUR - PIC","A GAS - BUR","F MAO - IRI","A BUR - PIC"],"GERMANY":["A MUN - BOH","F HOL S A BEL","A RUH - MUN","A BEL S F HOL","F DEN S F SKA - SWE","F SKA - SWE"],"ITALY":["F ION S A ALB - GRE","F EAS S F GRE - AEG","A ALB - GRE","A SER - BUL","A VEN H"],"RUSSIA":["A RUM S A BUD","F SWE S F STP\/NC - NWY","A SEV - ARM","A NWY - FIN","A BUD S A WAR - GAL","A WAR - GAL","A MOS - WAR","F STP\/NC - NWY"],"TURKEY":["A CON - SMY","A BUL H","F AEG S A BUL","A ARM S A CON - SMY"]},"results":{"A VIE":["bounce"],"F GRE":[],"A TRI":["bounce"],"F ENG":[],"F EDI":[],"F NTH":[],"A PAR":[],"A BRE":[],"F MAO":[],"A GAS":[],"A BUR":[],"A MUN":[],"F HOL":[],"A RUH":[],"A BEL":[],"F DEN":[],"F SKA":[],"F ION":[],"F EAS":[],"A ALB":[],"A SER":["bounce"],"A VEN":[],"A RUM":[],"F SWE":["cut","dislodged"],"A SEV":["bounce"],"A NWY":[],"A BUD":["cut"],"A MOS":["bounce"],"F STP\/NC":[],"A WAR":["bounce"],"A CON":[],"A BUL":[],"A ARM":["cut"],"F AEG":["cut","dislodged"]},"messages":[{"sender":"AUSTRIA","recipient":"ITALY","time_sent":679060,"phase":"S1904M","message":"Greece is supporting Ionian to Aegean if you want that"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":679971,"phase":"S1904M","message":"If you attack Norway I will take Sweden then I will support you into Norway in the Autumn."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":680141,"phase":"S1904M","message":"Not sure I should believe you."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":684683,"phase":"S1904M","message":"I think I will convoy A Alb to Syria. I will support F Gre-Bul. If it works, we can take Greece back in the fall.<br \/><br \/>Please also think about A Vie-Gal; A Tri-Bud. I think that will bounce out the Russian from Gal."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":685070,"phase":"S1904M","message":"Does that mean you don't believe I am attacking Sweden or you don't believe I will support you into Norway next move?<br \/>Looking at Scandavia we either share it or get nothing, it is a really good choice to share and Russia will end up 2 centres less.<br \/>You cannot take Norway without me and I cannot get Sweden without you.<br \/>I am not going to attack you or France, Italy would take too long and well we either do these moves or probably Russia wins.<br \/>You have nothing to lose and everything to gain!"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":685552,"phase":"S1904M","message":"You did not want to work together earlier. Why should I believe you now?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":686587,"phase":"S1904M","message":"What is your Ven army going to do?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":687252,"phase":"S1904M","message":"I'm moving it to Tyrolia unless you prefer me holding in Venice or have a plan how it could otherwise help us."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":692265,"phase":"S1904M","message":"Just forget it then... who cares... do whatever you want!"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":697430,"phase":"S1904M","message":"You have an interesting style."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":699068,"phase":"S1904M","message":"Germany tells me they are attacking Sweden."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":702838,"phase":"S1904M","message":"I'm sorry for being wishy washy, but maybe the convoy to Syria is not the best move at this time. We could possibly blow up the Turkish fleetand could guarantee Bul falls to us unless Russia interferes. What do you think of this plan?:<br \/><br \/>Spring: A Alb-Gre; F Ion S Alb-Gre; F Gre-Aeg; F Eas S Gre-Aeg.<br \/><br \/>Worst case, Turkey retreats the fleet to Smyrna or Con. Best case, the fleet is forced to disband.<br \/><br \/>Fall: AUSTRIAN F Aeg -Con (cuts support); A Ser-S Gre-Bul; A Gre-Bul OR<br \/>F Aeg-Bul; A Gre S Aeg-Bul; A Ser S Aeg-Bul)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":710183,"phase":"S1904M","message":"I'm not going to help Austria attack you. But I need Austrian help to kill the Turk. There's a good chance you could make it to Armenia this spring, I'll wager."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":710218,"phase":"S1904M","message":"If you get there, support me to Smyrna, and I'll support you to Ankara."},{"sender":"ITALY","recipient":"FRANCE","time_sent":710734,"phase":"S1904M","message":"This is a funny game. England is shitting bricks because of the German fleets. Germany is shitting bricks because of the French armies. <br \/><br \/>Thank you for honoring the dmz agreement between us. It has worked out well for us both so far. Good luck in your upcoming moves."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":718644,"phase":"S1904M","message":"soudns good. im trying for it - will be hard without the fleet"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":720389,"phase":"S1904M","message":"Hopefully, he will try to cover Smyrna. I'm letting him in there."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":720556,"phase":"S1904M","message":"Have not heard from you. I will order F Eas S Gre-Aeg; A Alb-Gre; F Ion S Alb-Gre; A Ser - Bul. Hopefully, you get this in time to respond. I will check my messages again tomorrow a.m."},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":726889,"phase":"S1904M","message":"i heard you are going to try to take swe. i think this would be foolish as together we can crush england and I am no threat to you. stay my ally, and I will agree to split eng 2\/1 in your favor, and help you against france"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":736286,"phase":"S1904M","message":"I gave you Sweden in the first place, now I want it back, that is fair don't you think?"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":736572,"phase":"S1904M","message":"Don't know why you would want to ally with Russia, that is a recipe to lose. You must see you have an opportunity here, this move is crucial. Help me take Sweden, Russia just proposed to share your country with me, but I refused."},{"sender":"FRANCE","recipient":"ITALY","time_sent":745412,"phase":"S1904M","message":"For you as well my loyal friend!"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":745594,"phase":"S1904M","message":"Should we try again or are you giving support?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":758810,"phase":"S1904M","message":"Yeah I am basically going to be bouncing Russia here"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":758825,"phase":"S1904M","message":"And cutting Bul"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":759959,"phase":"S1904M","message":"Giving support"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":760743,"phase":"S1904M","message":"Greece is moving to the Aegean though"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":761156,"phase":"S1904M","message":"Ok, my support for F Gre-Aeg is in for you. Serbia is cutting Bul."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":761192,"phase":"S1904M","message":"Awesome"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":763272,"phase":"S1904M","message":"Yeah sorry I wasn't on last night. Are you in the US? I was at a playoff hockey game that went really late"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":763445,"phase":"S1904M","message":"alright"}]},{"name":"S1904R","state":{"timestamp":1537459327459716,"zobrist_hash":"4620280055137301299","note":"","name":"S1904R","units":{"AUSTRIA":["A VIE","A TRI","F AEG"],"ENGLAND":["F ENG","F EDI","F NTH"],"FRANCE":["A PAR","A BRE","F IRI","A BUR","A PIC"],"GERMANY":["F HOL","A BEL","F DEN","A BOH","A MUN","F SWE"],"ITALY":["F ION","F EAS","A SER","A VEN","A GRE"],"RUSSIA":["A RUM","A SEV","A BUD","A MOS","A WAR","A FIN","F NWY","*F SWE"],"TURKEY":["A BUL","A ARM","A SMY","*F AEG"]},"centers":{"AUSTRIA":["TRI","VIE","GRE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["MAR","PAR","POR","SPA","BRE"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","BUD","NWY"],"TURKEY":["ANK","CON","SMY","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","AEG"],"ENGLAND":["LON","LVP","YOR","NWG","ENG","EDI","NTH"],"FRANCE":["PAR","POR","SPA","BRE","MAO","GAS","MAR","IRI","BUR","PIC"],"GERMANY":["BER","KIE","HOL","HEL","RUH","BEL","DEN","SKA","BOH","MUN","SWE"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION","EAS","TRI","ALB","SER","GRE"],"RUSSIA":["WAR","MOS","STP","UKR","BOT","RUM","GAL","SEV","BUD","FIN","NWY"],"TURKEY":["ANK","CON","BLA","BUL","ARM","SMY"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["F SWE R BAL"],"TURKEY":["F AEG R CON"]},"results":{"F SWE":[],"F AEG":[]},"messages":[{"sender":"AUSTRIA","recipient":"ITALY","time_sent":764766,"phase":"S1904R","message":"That worked out"},{"sender":"FRANCE","recipient":"ITALY","time_sent":765532,"phase":"S1904R","message":"your successes are impressive"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":765839,"phase":"S1904R","message":"What happened to joining forces?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":765859,"phase":"S1904R","message":"Did you not believe me?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":766737,"phase":"S1904R","message":"too bad, that was my fault. I shall retreat immediately"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":766988,"phase":"S1904R","message":"i do. thank you. im moving against him. want help agaisnt him as well?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":767288,"phase":"S1904R","message":"I assume you want me to break support from Denmark?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":777137,"phase":"S1904R","message":"It appears that the R\/G alliance is going to need a united front between Austria, Italy, and France."},{"sender":"FRANCE","recipient":"ITALY","time_sent":777430,"phase":"S1904R","message":"well, the bit that remains of Austria..."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":777944,"phase":"S1904R","message":"Yes, I'm in North Carolina. I live on a farm with goats and pigs. Didn't even turn on the TV yesterday and we rarely get a chance to go anywhere other than day trips. I love hockey though. Once went to a hockey game in Chamanoix, France, after skiing. The French skated rings around the Canadians and out scored them by five or six goals. But, the French skaters were wimps and the Canadians kicked their asses and got into a bunch of fights and penalties. I don't think Europeans are used to fighting. A funny game.<br \/><br \/>The next move you need to order F Aeg-Con to cut the Turk's support for Bul. (He'll retreat to Con.) I'll support myself from Greece to Bul with Serbia so you will not have to disband.<br \/><br \/>I'm moving to Tyrolia. Will support vie from their next spring.<br \/><br \/>I think you should order A Tri S Vie. A Vie S Ven-Tyo. The German is allied with Russia. Expect Bohemia to cooperate with Russia."},{"sender":"ITALY","recipient":"FRANCE","time_sent":778023,"phase":"S1904R","message":"True. He is hanging on by a thread."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":786522,"phase":"S1904R","message":"no i can take it back. if you took hel and NS, we can hit him hard next year"}]},{"name":"F1904M","state":{"timestamp":1537459327471699,"zobrist_hash":"8505355764121407935","note":"","name":"F1904M","units":{"AUSTRIA":["A VIE","A TRI","F AEG"],"ENGLAND":["F ENG","F EDI","F NTH"],"FRANCE":["A PAR","A BRE","F IRI","A BUR","A PIC"],"GERMANY":["F HOL","A BEL","F DEN","A BOH","A MUN","F SWE"],"ITALY":["F ION","F EAS","A SER","A VEN","A GRE"],"RUSSIA":["A RUM","A SEV","A BUD","A MOS","A WAR","A FIN","F NWY","F BAL"],"TURKEY":["A BUL","A ARM","A SMY","F CON"]},"centers":{"AUSTRIA":["TRI","VIE","GRE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["MAR","PAR","POR","SPA","BRE"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","BUD","NWY"],"TURKEY":["ANK","CON","SMY","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","AEG"],"ENGLAND":["LON","LVP","YOR","NWG","ENG","EDI","NTH"],"FRANCE":["PAR","POR","SPA","BRE","MAO","GAS","MAR","IRI","BUR","PIC"],"GERMANY":["BER","KIE","HOL","HEL","RUH","BEL","DEN","SKA","BOH","MUN","SWE"],"ITALY":["NAP","ROM","TYR","TUN","VEN","APU","ION","EAS","TRI","ALB","SER","GRE"],"RUSSIA":["WAR","MOS","STP","UKR","BOT","RUM","GAL","SEV","BUD","FIN","NWY","BAL"],"TURKEY":["ANK","BLA","BUL","ARM","SMY","CON"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE - BUD","A TRI - VIE","F AEG - CON"],"ENGLAND":["F ENG S F NTH - BEL","F EDI - NTH","F NTH - BEL"],"FRANCE":["A PAR - BUR","A BRE H","A PIC S F NTH - BEL","A BUR - RUH","F IRI - LVP"],"GERMANY":["F HOL S A BEL","A BEL H","F DEN S F SWE","A BOH - MUN","A MUN - BER","F SWE S F NTH - NWY"],"ITALY":["F ION H","F EAS - SMY","A SER S A GRE - BUL","A VEN - TYR","A GRE - BUL"],"RUSSIA":["A RUM S A BUD","A SEV - ARM","A BUD S A WAR - GAL","A WAR - PRU","A MOS - WAR","A FIN - SWE","F NWY S A FIN - SWE","F BAL S A FIN - SWE"],"TURKEY":["A BUL H","A ARM S A SMY","A SMY H","F CON S A BUL"]},"results":{"A VIE":["bounce"],"A TRI":["bounce"],"F AEG":["bounce"],"F ENG":[],"F EDI":[],"F NTH":[],"A PAR":[],"A BRE":[],"F IRI":[],"A BUR":[],"A PIC":[],"F HOL":[],"A BEL":["dislodged"],"F DEN":[],"A BOH":[],"A MUN":[],"F SWE":["void","dislodged"],"F ION":[],"F EAS":["bounce"],"A SER":[],"A VEN":[],"A GRE":[],"A RUM":[],"A SEV":["bounce"],"A BUD":["void"],"A MOS":[],"A WAR":[],"A FIN":[],"F NWY":[],"F BAL":[],"A BUL":["dislodged"],"A ARM":["cut"],"A SMY":[],"F CON":["cut"]},"messages":[{"sender":"FRANCE","recipient":"ENGLAND","time_sent":833066,"phase":"F1904M","message":"do you want to take belgium? i support your attack from north sea, he cannot defend it."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":847905,"phase":"F1904M","message":"That sounds good to me."},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":850357,"phase":"F1904M","message":"Ah, I forgot you had the retreat."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":851919,"phase":"F1904M","message":"done then"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":853063,"phase":"F1904M","message":"I have my order for A Gre-Bul; A Ser S Gre-Bul. Make sure you order F Aeg-Con to cut his probable support for A Bul. Hopefully, Russia will continue his policy of non-interference. <br \/><br \/>I am on a trip this weekend but I will access this with my smart phone."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":853334,"phase":"F1904M","message":"Did Germany stab you?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":853440,"phase":"F1904M","message":"If I can get Bul this turn, it will be mop up time for us against Turkey."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":853562,"phase":"F1904M","message":"I hope that isn't a French stab. I wouldn't like to see France ally with Germany and Russia."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":854100,"phase":"F1904M","message":"Yeah, me too. They claim to be retreating."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":854538,"phase":"F1904M","message":"England you need to ally with me. France will take Liverpool.<br \/>I will support North Sea to Norway.<br \/>You have to trust me or you lose everything."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":855170,"phase":"F1904M","message":"The move is made: Sweden supports North Sea to Norway<br \/>It's not guaranteed, he could support with Finland, but I think you will get it.<br \/>You move Edinburgh to Clyde, Channel to Wales and get back Liverpool next turn.<br \/>I will move into North Sea, France only has one fleet so far.<br \/><br \/>It's true I could stab you and attack England, but I won't, I don't enjoy playing like that and besides I really do need you."}]},{"name":"F1904R","state":{"timestamp":1537459327474208,"zobrist_hash":"6569841361482054665","note":"","name":"F1904R","units":{"AUSTRIA":["A VIE","A TRI","F AEG"],"ENGLAND":["F ENG","F NTH","F BEL"],"FRANCE":["A BRE","A PIC","A BUR","F LVP","A RUH"],"GERMANY":["F HOL","F DEN","A MUN","A BER","*F SWE"],"ITALY":["F ION","F EAS","A SER","A TYR","A BUL"],"RUSSIA":["A RUM","A SEV","A BUD","F NWY","F BAL","A WAR","A PRU","A SWE"],"TURKEY":["A ARM","A SMY","F CON"]},"centers":{"AUSTRIA":["TRI","VIE","GRE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["MAR","PAR","POR","SPA","BRE"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN","TUN","SER"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","BUD","NWY"],"TURKEY":["ANK","CON","SMY","BUL"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","AEG"],"ENGLAND":["LON","YOR","NWG","ENG","EDI","NTH","BEL"],"FRANCE":["PAR","POR","SPA","BRE","MAO","GAS","MAR","IRI","PIC","BUR","LVP","RUH"],"GERMANY":["KIE","HOL","HEL","DEN","SKA","BOH","MUN","BER"],"ITALY":["NAP","ROM","TUN","VEN","APU","ION","EAS","TRI","ALB","SER","GRE","TYR","BUL"],"RUSSIA":["MOS","STP","UKR","BOT","RUM","GAL","SEV","BUD","FIN","NWY","BAL","WAR","PRU","SWE"],"TURKEY":["ANK","BLA","ARM","SMY","CON"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":["F SWE R BOT"],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"F SWE":[],"A BEL":["disband"],"A BUL":["disband"]},"messages":[{"sender":"AUSTRIA","recipient":"ITALY","time_sent":928511,"phase":"F1904R","message":"Will you support me into Budapest from Trieste?"}]},{"name":"W1904A","state":{"timestamp":1537459327475681,"zobrist_hash":"7444462480205395719","note":"","name":"W1904A","units":{"AUSTRIA":["A VIE","A TRI","F AEG"],"ENGLAND":["F ENG","F NTH","F BEL"],"FRANCE":["A BRE","A PIC","A BUR","F LVP","A RUH"],"GERMANY":["F HOL","F DEN","A MUN","A BER","F BOT"],"ITALY":["F ION","F EAS","A SER","A TYR","A BUL"],"RUSSIA":["A RUM","A SEV","A BUD","F NWY","F BAL","A WAR","A PRU","A SWE"],"TURKEY":["A ARM","A SMY","F CON"]},"centers":{"AUSTRIA":["TRI","VIE","GRE"],"ENGLAND":["EDI","LON","BEL"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","BUD","NWY"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","AEG"],"ENGLAND":["LON","YOR","NWG","ENG","EDI","NTH","BEL"],"FRANCE":["PAR","POR","SPA","BRE","MAO","GAS","MAR","IRI","PIC","BUR","LVP","RUH"],"GERMANY":["KIE","HOL","HEL","DEN","SKA","BOH","MUN","BER","BOT"],"ITALY":["NAP","ROM","TUN","VEN","APU","ION","EAS","TRI","ALB","SER","GRE","TYR","BUL"],"RUSSIA":["MOS","STP","UKR","RUM","GAL","SEV","BUD","FIN","NWY","BAL","WAR","PRU","SWE"],"TURKEY":["ANK","BLA","ARM","SMY","CON"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":1,"homes":["MAR","PAR"]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":1,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["F MAR B"],"GERMANY":[],"ITALY":["F NAP B"],"RUSSIA":[],"TURKEY":[]},"results":{"F MAR":[""],"F NAP":[""]},"messages":[{"sender":"FRANCE","recipient":"ITALY","time_sent":933021,"phase":"W1904A","message":"hey you, just to inform you, i would need to build a fleet in marseilles in order to bring it round into mao. is that okay with you?"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":933101,"phase":"W1904A","message":"Seriously?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":935039,"phase":"W1904A","message":"sorry, i couldn't resist... but i recompensated you in a way! And i would offer you holland as well."},{"sender":"ITALY","recipient":"FRANCE","time_sent":935075,"phase":"W1904A","message":"I figured you might need another fleet. Thanks for the notice. Yes, with your stipulation, I am fine. I want to stay on good terms."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":937322,"phase":"W1904A","message":"Yes, and also to Con. Just ask for Greece in exchange."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":938763,"phase":"W1904A","message":"Deal"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":940839,"phase":"W1904A","message":"I suppose I might as well keep believing you."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":942258,"phase":"W1904A","message":"Yes, and also to Con. Just ask for Greece in exchange."}]},{"name":"S1905M","state":{"timestamp":1537459327488066,"zobrist_hash":"4310565217717138903","note":"","name":"S1905M","units":{"AUSTRIA":["A VIE","A TRI","F AEG"],"ENGLAND":["F ENG","F NTH","F BEL"],"FRANCE":["A BRE","A PIC","A BUR","F LVP","A RUH","F MAR"],"GERMANY":["F HOL","F DEN","A MUN","A BER","F BOT"],"ITALY":["F ION","F EAS","A SER","A TYR","A BUL","F NAP"],"RUSSIA":["A RUM","A SEV","A BUD","F NWY","F BAL","A WAR","A PRU","A SWE"],"TURKEY":["A ARM","A SMY","F CON"]},"centers":{"AUSTRIA":["TRI","VIE","GRE"],"ENGLAND":["EDI","LON","BEL"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","BUD","NWY"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","AEG"],"ENGLAND":["LON","YOR","NWG","ENG","EDI","NTH","BEL"],"FRANCE":["PAR","POR","SPA","BRE","MAO","GAS","MAR","IRI","PIC","BUR","LVP","RUH"],"GERMANY":["KIE","HOL","HEL","DEN","SKA","BOH","MUN","BER","BOT"],"ITALY":["NAP","ROM","TUN","VEN","APU","ION","EAS","TRI","ALB","SER","GRE","TYR","BUL"],"RUSSIA":["MOS","STP","UKR","RUM","GAL","SEV","BUD","FIN","NWY","BAL","WAR","PRU","SWE"],"TURKEY":["ANK","BLA","ARM","SMY","CON"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE S A TRI - BUD","A TRI - BUD","F AEG - CON"],"ENGLAND":["F ENG - MAO","F NTH S F BEL - HOL","F BEL - HOL"],"FRANCE":["A BRE S A PIC","A PIC S A BRE","A BUR - MUN","A RUH S A BUR - MUN","F LVP - CLY","F MAR - SPA\/SC"],"GERMANY":["F HOL - NTH","F DEN - BAL","A BER H","A MUN - RUH","F BOT - STP\/SC"],"ITALY":["F ION - GRE","F EAS - SMY","A SER S A TRI - BUD","A TYR H","A BUL S F AEG - CON","F NAP - TYS"],"RUSSIA":["A RUM S A BUD","A SEV - ARM","A BUD S A RUM","F NWY - STP\/NC","F BAL S A PRU - BER","A PRU - BER","A WAR - GAL","A SWE H"],"TURKEY":["A ARM S A SMY","A SMY S F CON","F CON S A SMY"]},"results":{"A VIE":[],"A TRI":[],"F AEG":[],"F ENG":[],"F NTH":[],"F BEL":[],"A BRE":[],"A PIC":[],"A BUR":[],"F LVP":[],"A RUH":[],"F MAR":[],"F HOL":["bounce","dislodged"],"F DEN":["bounce"],"A MUN":["bounce","dislodged"],"A BER":[],"F BOT":["bounce"],"F ION":[],"F EAS":["bounce"],"A SER":[],"A TYR":[],"A BUL":[],"F NAP":[],"A RUM":[],"A SEV":["bounce"],"A BUD":["cut","dislodged"],"F NWY":["bounce"],"F BAL":["cut"],"A WAR":[],"A PRU":["bounce"],"A SWE":[],"A ARM":["cut"],"A SMY":["cut"],"F CON":["cut","dislodged"]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":942558,"phase":"S1905M","message":"Excellent! I see you in Rumania soon. I just need some help killing off the Turk. Then, for you, it's on to Black Sea and Sev while I head towards the western Med."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":943008,"phase":"S1905M","message":"I am ordering A Bul and A Ser to support F Aeg-Con, and A Tri-Bud."},{"sender":"ITALY","recipient":"GERMANY","time_sent":943105,"phase":"S1905M","message":"Do need support for Munich hold?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":943724,"phase":"S1905M","message":"would you give me support taking munich from burgundy?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":946807,"phase":"S1905M","message":"I have no great love for the Kaiser, but don't see a good reason to start a war in the west with Germany. Especially with the French fleet in the Med. Once your in Wes, I see no reason why we couldn't ally and move against Germany."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":949424,"phase":"S1905M","message":"Was Liverpool a trade for bel? As usual, I'm Not hearing from Germany."},{"sender":"GERMANY","recipient":"ITALY","time_sent":950493,"phase":"S1905M","message":"Yes that would be good, thanks."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":950946,"phase":"S1905M","message":"Are you playing both England and France?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":951625,"phase":"S1905M","message":"Germany wants support for Munich.<br \/><br \/>France wants support into it. Do you have a preference?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":958095,"phase":"S1905M","message":"Nope, they stole it from me :("},{"sender":"ENGLAND","recipient":"ITALY","time_sent":958136,"phase":"S1905M","message":"Germany continues to want me to help them against Russia. They don't talk too much."},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":958192,"phase":"S1905M","message":"I don't know how to do that."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":959783,"phase":"S1905M","message":"Then, you want me to help Germany against the French?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":961017,"phase":"S1905M","message":"Then, you want me to help Germany against the French?"},{"sender":"GERMANY","recipient":"GLOBAL","time_sent":963083,"phase":"S1905M","message":"Is it possible that England and France are the same person playing both countries?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":966943,"phase":"S1905M","message":"your call. you need to deal them better than I and france looks like hes aggro to you now"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":968981,"phase":"S1905M","message":"Germany seems doomed. I will move to protect myself."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":969011,"phase":"S1905M","message":"I'm kind of stuck here but I think I need to go against Germany."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":969226,"phase":"S1905M","message":"Just hold onto Bel and make France take Munich with two units. I'm not supporting the French into Munich so he must use A Ruhr to take it. That should slow the French down and make him play more honest. Are you going to cooperate with Russia against the French? Russia needs you."},{"sender":"ITALY","recipient":"GERMANY","time_sent":974322,"phase":"S1905M","message":"Sorry. Treachery to s afoot. I cannot provide the support this turn."},{"sender":"GERMANY","recipient":"ITALY","time_sent":994073,"phase":"S1905M","message":"12 inches"},{"sender":"FRANCE","recipient":"GLOBAL","time_sent":999504,"phase":"S1905M","message":"apparently not..."},{"sender":"FRANCE","recipient":"ITALY","time_sent":999540,"phase":"S1905M","message":"alright, i accept your worries, thought germany is pretty dead already"},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":1016921,"phase":"S1905M","message":"Ahh convincing retort from the French! Tune in tonight to see if Germany can recover"},{"sender":"ITALY","recipient":"GLOBAL","time_sent":1019072,"phase":"S1905M","message":"It seems the English taking Brest, and the French Liverpool, are a clever subterfuge for nefarious machinations of we rubes."},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":1022991,"phase":"S1905M","message":"Too bad England lost Brest though"}]},{"name":"S1905R","state":{"timestamp":1537459327490692,"zobrist_hash":"2411975831117752644","note":"","name":"S1905R","units":{"AUSTRIA":["A VIE","A BUD","F CON"],"ENGLAND":["F NTH","F MAO","F HOL"],"FRANCE":["A BRE","A PIC","A RUH","A MUN","F CLY","F SPA\/SC"],"GERMANY":["F DEN","A BER","F BOT","*F HOL","*A MUN"],"ITALY":["F EAS","A SER","A TYR","A BUL","F GRE","F TYS"],"RUSSIA":["A RUM","A SEV","F NWY","F BAL","A PRU","A SWE","A GAL"],"TURKEY":["A ARM","A SMY","*F CON"]},"centers":{"AUSTRIA":["TRI","VIE","GRE"],"ENGLAND":["EDI","LON","BEL"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","BUD","NWY"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","AEG","BUD","CON"],"ENGLAND":["LON","YOR","NWG","ENG","EDI","NTH","BEL","MAO","HOL"],"FRANCE":["PAR","POR","BRE","GAS","MAR","IRI","PIC","BUR","LVP","RUH","MUN","CLY","SPA"],"GERMANY":["KIE","HEL","DEN","SKA","BOH","BER","BOT"],"ITALY":["NAP","ROM","TUN","VEN","APU","ION","EAS","TRI","ALB","SER","TYR","BUL","GRE","TYS"],"RUSSIA":["MOS","STP","UKR","RUM","SEV","FIN","NWY","BAL","WAR","PRU","SWE","GAL"],"TURKEY":["ANK","BLA","ARM","SMY"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":["A MUN R KIE","F HOL R HEL"],"ITALY":[],"RUSSIA":[],"TURKEY":["F CON D"]},"results":{"F HOL":[],"A MUN":[],"F CON":["disband"],"A BUD":["disband"]},"messages":[{"sender":"FRANCE","recipient":"ITALY","time_sent":1029086,"phase":"S1905R","message":"hey! I thought we were doing great as allies..."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":1030050,"phase":"S1905R","message":"I tried :) I'll bounce you in Edi and move up to Irish Sea. Sound good?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":1031881,"phase":"S1905R","message":"sure"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1034149,"phase":"S1905R","message":"We are. I am moving F Tyrrhenian Sea to Ionian Sea this fall. We are totally good. <br \/><br \/>Please try to not need Marseilles anymore for raising navies. With Munich and Belgium in your pocket, maybe you can take a chance and leave Brest open for your next fleet. Your history of sitting in Brest means he is unlikely to guess Brest. Let him take Portugal. You can root him out and blow up his rogue fleet next year."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1034321,"phase":"S1905R","message":"Good move."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":1035390,"phase":"S1905R","message":"why the stab?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1035408,"phase":"S1905R","message":"im not going to attack you know so you can hit aus\/italy as italy stabbed me"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":1037054,"phase":"S1905R","message":"I mean... you don't have to try for Edi ;)"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1040162,"phase":"S1905R","message":"I don't really consider that support of Austria back into his home center much of a stab. The reason I have for ordering the support, which he requested, are good ones. First, it gets him further from Venice. Second, I need Austria as an ally to finish off Turkey, which is a mutually beneficial prospect for both of us. Third, you already have 8 centers and are taking Berlin next turn so this keeps you from exploding in growth and leaving we minor powers in the dust. Fourth, Austria is still weak and between us. And fifth, I figured you would understand why I had to do it."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1040844,"phase":"S1905R","message":"If you can make amends with England, i will support you to Munich next turn. Just get him to write to me that you and he are now allied. <br \/><br \/>If you can patch things up with him, I suggest Berlin-Munich might be your best move. You keep Holland, and he keeps France from taking Edi and Bel from him. Meanwhile, you need to make Russia retreat from Baltic Sea or your defense of your home dots will be very difficult. <br \/><br \/>If you like, I can give you some good tactics if you are willing to accept my advice. I don't want to see you and England collapse. It is not good."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":1041035,"phase":"S1905R","message":"got you"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1041117,"phase":"S1905R","message":"I wrote to Germany and told him he needs to patch things up with you. France is going to take Belgium next turn. You might as well see if you can get German help and save Edi and Bel. Holland is not worth losing Belgium. <br \/><br \/>If you can ally with Germany, he can cut Ruhr and you can move from Hol-Bel. Get him to disband his fleets. <br \/><br \/>You guys should be able to work out a deal that is favorable to England. <br \/><br \/>FWIW, I am attempting to discourage France from building another fleet in Marseilles. I don't know if it will work, but maybe Brest is left open for his fleet build. That is better for you than Portugal, which is a death trap. You might ask why I am saying this. It is because if France, gets two or three builds, I am going to have to defend against France because he will be coming into the Med."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1041363,"phase":"S1905R","message":"I think the Turk will retreat to Ankara and make these orders next year: <br \/><br \/>A Arm-Smy; A Smy-Con; A Ank S Smy-Con.<br \/><br \/>I am worried Russia will order A Rum-Bud to cut support for Con. Therefore, I think these are the best orders: <br \/><br \/>F Con-Smy; F Eas S Con-Smy.<br \/><br \/>That will make the Turkish expected attack bounce.<br \/><br \/>As for the north, you should try for Galacia (A Vie-Gal; A Bud S Vie-Gal). I will cut support from Rum. If I hit Rum, and you hit Gal, Budapest is safe and you can build in either Trieste or Vienna.<br \/><br \/>What do you think?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":1049869,"phase":"S1905R","message":"I have yet to hear from Germany. I'll let you know."},{"sender":"GERMANY","recipient":"ITALY","time_sent":1050665,"phase":"S1905R","message":"What can I say, England is an idiot, I offered to ally ages ago, he refused prefering France. I told him he would lose to Russia and that's how it's turning out, you might manage a 3 way draw, but I am finished."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1099801,"phase":"S1905R","message":"I don't think it is as bad as you think. Austria and I are pushing back against Russia. If you and England work together you can stymie France, and then, I will swing west to force him to deal with me. <br \/><br \/>You are only going to lose one if these things happen.<br \/><br \/>Retreat A Munich to Kiel and F Hol to Hel. <br \/><br \/>Then, Kiel-Ruh. England orders F Hol-Bel. I support Berlin -Munich. I think that works. If England lucks into Brest, then France only has minimal gains and your position is pretty good."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1099909,"phase":"S1905R","message":"Also, you need to take back Baltic Sea. F Bot-Bal; F Den S Bot - Bal; F Hel S Den."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1105302,"phase":"S1905R","message":"I heard from Germany last night. He is upset you didn't take up his alliance offer last season. Some people are more emotional and less rational. I suggest you approach him with an apology and tell him he was right and you were wrong for trusting France. France is now fucking both of you. Therefore, you have a common opponent against whom you can direct your ire. Use this. I think it will work on the emotional side. <br \/><br \/>You are going to have to offer Germany Holland back along with the apology. With France just taking Bel if you do nothing, it is worthless to you anyway except for the purpose of building France into a force that cannot be reckoned with until you die."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":1108731,"phase":"S1905R","message":"I'll see what I can do"}]},{"name":"F1905M","state":{"timestamp":1537459327501361,"zobrist_hash":"2599241509863897021","note":"","name":"F1905M","units":{"AUSTRIA":["A VIE","A BUD","F CON"],"ENGLAND":["F NTH","F MAO","F HOL"],"FRANCE":["A BRE","A PIC","A RUH","A MUN","F CLY","F SPA\/SC"],"GERMANY":["F DEN","A BER","F BOT","A KIE","F HEL"],"ITALY":["F EAS","A SER","A TYR","A BUL","F GRE","F TYS"],"RUSSIA":["A RUM","A SEV","F NWY","F BAL","A PRU","A SWE","A GAL"],"TURKEY":["A ARM","A SMY"]},"centers":{"AUSTRIA":["TRI","VIE","GRE"],"ENGLAND":["EDI","LON","BEL"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","BUD","NWY"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","AEG","BUD","CON"],"ENGLAND":["LON","YOR","NWG","ENG","EDI","NTH","BEL","MAO","HOL"],"FRANCE":["PAR","POR","BRE","GAS","MAR","IRI","PIC","BUR","LVP","RUH","MUN","CLY","SPA"],"GERMANY":["DEN","SKA","BOH","BER","BOT","KIE","HEL"],"ITALY":["NAP","ROM","TUN","VEN","APU","ION","EAS","TRI","ALB","SER","TYR","BUL","GRE","TYS"],"RUSSIA":["MOS","STP","UKR","RUM","SEV","FIN","NWY","BAL","WAR","PRU","SWE","GAL"],"TURKEY":["ANK","BLA","ARM","SMY"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE - GAL","F CON S F EAS - SMY","A BUD H"],"ENGLAND":["F NTH - EDI","F HOL - BEL","F MAO - IRI"],"FRANCE":["A BRE H","A PIC - BEL","A RUH S A PIC - BEL","F CLY - EDI","A MUN H","F SPA\/SC - MAO"],"GERMANY":["F DEN - NTH","A BER H","F BOT - STP\/SC","A KIE S F HEL - HOL","F HEL - HOL"],"ITALY":["F EAS - SMY","A SER S A BUD","A TYR H","A BUL - RUM","F TYS - ION","F GRE H"],"RUSSIA":["A RUM S A GAL - BUD","A SEV S A RUM","F NWY - STP\/NC","F BAL - BER","A PRU S F BAL - BER","A SWE - DEN","A GAL - BUD"],"TURKEY":["A ARM H","A SMY H"]},"results":{"A VIE":["bounce"],"A BUD":[],"F CON":[],"F NTH":["bounce"],"F MAO":[],"F HOL":["bounce","dislodged"],"A BRE":[],"A PIC":[],"A RUH":[],"A MUN":[],"F CLY":["bounce"],"F SPA\/SC":[],"F DEN":["bounce"],"A BER":["dislodged"],"F BOT":["bounce"],"A KIE":[],"F HEL":[],"F EAS":[],"A SER":[],"A TYR":[],"A BUL":["bounce"],"F GRE":[],"F TYS":[],"A RUM":["cut"],"A SEV":[],"F NWY":["bounce"],"F BAL":[],"A PRU":[],"A SWE":["bounce"],"A GAL":["bounce"],"A ARM":[],"A SMY":["dislodged"]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1116065,"phase":"F1905M","message":"The Sultan did not send in an order last phase. That was a nice break. Please let me know if you will order as follows: <br \/><br \/>A Vie-Gal; A Con S ITALIAN F Eas-Smy. \\<br \/><br \/>If so, I will order A Bul-Rum giving you a shot at taking Galacia with Vienna. If you can build in Vienna instead of Trieste, you will get Rumania and maybe Sevastapol next year."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1116529,"phase":"F1905M","message":"You want Con to support what?"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1124575,"phase":"F1905M","message":"Please order F Con to support Fleet Eastern Med to Smyrna. Maybe Turkey will miss the next turn too."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1126199,"phase":"F1905M","message":"Ok. Turns are waiting on you now"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":1126492,"phase":"F1905M","message":"Nothing yet."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1130975,"phase":"F1905M","message":"Maybe he gave up. He did retreat to Kiel and Helgoland. Hmmm. I'm going to offer Germany to support A Berlin-Munich if he will order A Kiel to Ruhr in exchange for it. Those moves guarantee him Munich back. If he agrees, I will let you know.<br \/><br \/>Please bounce the French out of Belgium. A Pic-Bel is assured. I'm guessing France covers Brest and Spain so maybe Portugal is your best shot in Iberia. Maybe you bounce him out of Bel and get Portugal and Holland. That seems miraculous, but stranger things have happened."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1131016,"phase":"F1905M","message":"I'm waiting to see if Germany wants support to Munich. France is about to explode."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1131030,"phase":"F1905M","message":"Thank you."},{"sender":"ITALY","recipient":"GERMANY","time_sent":1131161,"phase":"F1905M","message":"Hello, I am willing to order A Tyrolia supports Berlin-Munich. In exchange for this order, I am requesting you order A Kiel-Ruhr. This move helps you guarantee that you get back Munich, and also keeps the French from taking Belgium. (I have persuaded England to move F Hol-Bel.) Please let me know if you would like to accept my support with the conditions provided. Thank you."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1134106,"phase":"F1905M","message":"Solid."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1185380,"phase":"F1905M","message":"Sorry, but my diplomacy hasn't worked. Germany isn't talking to me."}]},{"name":"F1905R","state":{"timestamp":1537459327503669,"zobrist_hash":"4548428095288800148","note":"","name":"F1905R","units":{"AUSTRIA":["A VIE","A BUD","F CON"],"ENGLAND":["F NTH","F IRI"],"FRANCE":["A BRE","A RUH","A MUN","F CLY","A BEL","F MAO"],"GERMANY":["F DEN","F BOT","A KIE","F HOL","*A BER"],"ITALY":["A SER","A TYR","A BUL","F GRE","F SMY","F ION"],"RUSSIA":["A RUM","A SEV","F NWY","A PRU","A SWE","A GAL","F BER"],"TURKEY":["A ARM","*A SMY"]},"centers":{"AUSTRIA":["TRI","VIE","GRE"],"ENGLAND":["EDI","LON","BEL"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","BUD","NWY"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","AEG","BUD","CON"],"ENGLAND":["LON","YOR","NWG","ENG","EDI","NTH","IRI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","BUR","LVP","RUH","MUN","CLY","SPA","BEL","MAO"],"GERMANY":["DEN","SKA","BOH","BOT","KIE","HEL","HOL"],"ITALY":["NAP","ROM","TUN","VEN","APU","EAS","TRI","ALB","SER","TYR","BUL","GRE","TYS","SMY","ION"],"RUSSIA":["MOS","STP","UKR","RUM","SEV","FIN","NWY","BAL","WAR","PRU","SWE","GAL","BER"],"TURKEY":["ANK","BLA","ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":["A BER R SIL"],"ITALY":[],"RUSSIA":[],"TURKEY":["A SMY D"]},"results":{"A BER":[],"A SMY":["disband"],"F HOL":["disband"]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":1186634,"phase":"F1905R","message":"I think it's time for us to ally."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1192252,"phase":"F1905R","message":"Let's talk about our upcoming strategy. I would like to have a contiguous force if possible, and it is in our best interest to keep our armies moving."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1192283,"phase":"F1905R","message":"I propose that my army in Trieste take Serbia and I support Serbia into Rum."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1192336,"phase":"F1905R","message":"I would also like to take Ankara if you will move Smyrna back into the Med once Turkey is eliminated."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1195322,"phase":"F1905R","message":"I was thinking you might move F Con-Black Sea and I would move F Smy-Con. I would support you into Ankara in the fall (eliminating the Turk) and trade Con for Ank. I agree one of us needs to take Rumania. My idea would be to support you into it so you can spearhead an attack against Russia while I go west against France. <br \/><br \/>You can eventually use F Black Sea with great affect to take Sevastapol and convoy armies into the southern coast of Russia.<br \/><br \/>Both pick up a build next year. We will make Serbia a DMZ once Rumania is secure.<br \/><br \/>Strategically, it doesn't make any sense for either of us to stab the other. It would just open the door for the larger powers of France and Russia to feast on the back of the power who initiates the stab. Rather, we both benefit by working together against Russia and France. We are pretty well balanced to do so."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1195418,"phase":"F1905R","message":"Why didn't you support yourself to Galacia last turn?"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1195605,"phase":"F1905R","message":"No, I have no plans to stab you, but I think you would agree that it's better for us to have our forces consolidated than spread out."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1196824,"phase":"F1905R","message":"Yes, I agree. But I will soon have to move most of my units west to deal with France and I don't see how you can help in the Med except maybe by eventually moving an army or two against whatever he captures in Germany. Our strategy necessarily makes you the land power and me the sea power so I think it may be more stable for me to stay south and you to expand into Russia."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1197633,"phase":"F1905R","message":"100% agree"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1197884,"phase":"F1905R","message":"Then I will take Rum and Ank (while you take Con) and then once Turk is eliminated I will move to the BS and use that to take Sev. <br \/><br \/>While that is happening, you will move west to take on France. I suggest that you Move Greece and even try and convoy Serbia out of the balkans region to get a start, I will really only need your fleet in Smyrna and your A Bulgaria to get us rolling on Russia."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1209652,"phase":"F1905R","message":"I'm going to build A Nap and convoy it to Greece and then to Smyrna, where it will remain indefinitely, or maybe go to Armenia if needed to support your army to Sev. <br \/><br \/>I will redeploy A Serbia west, as you suggested, but for now, I need the army in Serbia to block Rumania and to support A Bud to Rum, and\/or Trieste to Bud."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1209833,"phase":"F1905R","message":"I guess you're going to need to build in Marseilles again. I want us to keep doing our thing."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1209921,"phase":"F1905R","message":"That sucked. Maybe you can ally with Russia. He seems to be pretty reasonable as an ally. He could certainly use your help against the French."},{"sender":"FRANCE","recipient":"ITALY","time_sent":1210080,"phase":"F1905R","message":"not necessarily, England is defeated as is Germany and Turkey. Now we have three extremely well-balanced countries left but russia should be an easy target against the both of us in alliance. There is no reason to abandon this alliance from my part."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1210790,"phase":"F1905R","message":"Great! I was hoping you would want to work something out against the Russian. That argues for me staying allied with Austria. I think the two of us (A\/I) can make pretty good progress against him in the southeast this upcoming year."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":1210949,"phase":"F1905R","message":"When will you be ready to move against France?"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":1210987,"phase":"F1905R","message":"Still screwing with me... pretty sure Italy will start heading your way soon. They've been trying to advise me poorly against you."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1211031,"phase":"F1905R","message":"It starts in 1906."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":1212899,"phase":"F1905R","message":"what do you have in mind"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":1213015,"phase":"F1905R","message":"Nice. Hopefully I can at least be a distraction to help you surprise France."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1219109,"phase":"F1905R","message":"I'm counting on it."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1219150,"phase":"F1905R","message":"Working with you against France."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":1222844,"phase":"F1905R","message":"is it not too late for you to move on him? you have no units there"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1222940,"phase":"F1905R","message":"I am getting two builds, but I am thinking long term after Austria.Just saying maybe F Stp(nc) would be the best build for you."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":1222946,"phase":"F1905R","message":"i dont understand why you dont crush aus."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1223221,"phase":"F1905R","message":"Austria has been a good ally. I am having trouble with the morality of backstabbing him. But maybe it will not be much longer that I will have that struggle."}]},{"name":"W1905A","state":{"timestamp":1537459327505803,"zobrist_hash":"1245875534261384583","note":"","name":"W1905A","units":{"AUSTRIA":["A VIE","A BUD","F CON"],"ENGLAND":["F NTH","F IRI"],"FRANCE":["A BRE","A RUH","A MUN","F CLY","A BEL","F MAO"],"GERMANY":["F DEN","F BOT","A KIE","F HOL","A SIL"],"ITALY":["A SER","A TYR","A BUL","F GRE","F SMY","F ION"],"RUSSIA":["A RUM","A SEV","F NWY","A PRU","A SWE","A GAL","F BER"],"TURKEY":["A ARM"]},"centers":{"AUSTRIA":["TRI","VIE","BUD","CON"],"ENGLAND":["EDI","LON"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN"],"GERMANY":["KIE","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","NWY","BER"],"TURKEY":["ANK"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","AEG","BUD","CON"],"ENGLAND":["LON","YOR","NWG","ENG","EDI","NTH","IRI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","BUR","LVP","RUH","MUN","CLY","SPA","BEL","MAO"],"GERMANY":["DEN","SKA","BOH","BOT","KIE","HEL","HOL","SIL"],"ITALY":["NAP","ROM","TUN","VEN","APU","EAS","TRI","ALB","SER","TYR","BUL","GRE","TYS","SMY","ION"],"RUSSIA":["MOS","STP","UKR","RUM","SEV","FIN","NWY","BAL","WAR","PRU","SWE","GAL","BER"],"TURKEY":["ANK","BLA","ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":1,"homes":["TRI"]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":2,"homes":["MAR","PAR"]},"GERMANY":{"count":-2,"homes":[]},"ITALY":{"count":2,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":1,"homes":["MOS","STP","WAR"]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A TRI B"],"ENGLAND":[],"FRANCE":["F MAR B","A PAR B"],"GERMANY":["F BOT D","A SIL D"],"ITALY":["A NAP B","A ROM B"],"RUSSIA":["A WAR B"],"TURKEY":[]},"results":{"A TRI":[""],"F MAR":[""],"A PAR":[""],"F BOT":[""],"A SIL":[""],"A NAP":[""],"A ROM":[""],"A WAR":[""]},"messages":[{"sender":"FRANCE","recipient":"ITALY","time_sent":1226816,"phase":"W1905A","message":"okay, since i have two build which i didn't expect, would it be okay if i built another fleet at marseilles"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":1238225,"phase":"W1905A","message":"ok i understand"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1272321,"phase":"W1905A","message":"With the same stipulation as before?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1283740,"phase":"W1905A","message":"Of course, it moves directly into the Atlantic"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1283990,"phase":"W1905A","message":"Ok, we're good then. Sorry, I'm paranoid, lol."}]},{"name":"S1906M","state":{"timestamp":1537459327516226,"zobrist_hash":"4972876678689754789","note":"","name":"S1906M","units":{"AUSTRIA":["A VIE","A BUD","F CON","A TRI"],"ENGLAND":["F NTH","F IRI"],"FRANCE":["A BRE","A RUH","A MUN","F CLY","A BEL","F MAO","F MAR","A PAR"],"GERMANY":["F DEN","A KIE","F HOL"],"ITALY":["A SER","A TYR","A BUL","F GRE","F SMY","F ION","A NAP","A ROM"],"RUSSIA":["A RUM","A SEV","F NWY","A PRU","A SWE","A GAL","F BER","A WAR"],"TURKEY":["A ARM"]},"centers":{"AUSTRIA":["TRI","VIE","BUD","CON"],"ENGLAND":["EDI","LON"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN"],"GERMANY":["KIE","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","NWY","BER"],"TURKEY":["ANK"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","AEG","BUD","CON"],"ENGLAND":["LON","YOR","NWG","ENG","EDI","NTH","IRI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","BUR","LVP","RUH","MUN","CLY","SPA","BEL","MAO"],"GERMANY":["DEN","SKA","BOH","BOT","KIE","HEL","HOL","SIL"],"ITALY":["NAP","ROM","TUN","VEN","APU","EAS","TRI","ALB","SER","TYR","BUL","GRE","TYS","SMY","ION"],"RUSSIA":["MOS","STP","UKR","RUM","SEV","FIN","NWY","BAL","WAR","PRU","SWE","GAL","BER"],"TURKEY":["ANK","BLA","ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE - GAL","F CON - BLA","A BUD - RUM","A TRI - BUD"],"ENGLAND":["F NTH - ENG","F IRI - LVP"],"FRANCE":["A BRE - PIC","A RUH - HOL","F CLY - LVP","A MUN - KIE","A BEL S A RUH - HOL","F MAO - ENG","A PAR - BUR","F MAR - SPA\/SC"],"GERMANY":["F DEN S A KIE","A KIE S F HOL","F HOL H"],"ITALY":["A SER S A BUD - RUM","A TYR - VEN","A BUL S A BUD - RUM","F GRE - AEG","F ION C A NAP - GRE","F SMY - CON","A NAP - GRE VIA","A ROM - VEN"],"RUSSIA":["A RUM H","A SEV S A RUM","F NWY H","A PRU - BER","A SWE - DEN","A GAL S A RUM","F BER - BAL","A WAR - SIL"],"TURKEY":["A ARM H"]},"results":{"A VIE":["bounce"],"A BUD":[],"F CON":[],"A TRI":[],"F NTH":["bounce"],"F IRI":["bounce"],"A BRE":[],"A RUH":[],"A MUN":["bounce"],"F CLY":["bounce"],"A BEL":[],"F MAO":["bounce"],"F MAR":[],"A PAR":[],"F DEN":["cut"],"A KIE":["cut"],"F HOL":["dislodged"],"A SER":[],"A TYR":["bounce"],"A BUL":[],"F GRE":[],"F SMY":[],"F ION":[],"A NAP":[],"A ROM":["bounce"],"A RUM":["dislodged"],"A SEV":[],"F NWY":[],"A PRU":[],"A SWE":["bounce"],"A GAL":["cut"],"F BER":[],"A WAR":[],"A ARM":[]},"messages":[{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1284372,"phase":"S1906M","message":"Hello. I've ordered two supports for A Budapest to Rumania. Also, I am moving Smyrna to Con. You move F Con to Black Sea, and I will support you to Ankara in the fall to eliminate the Turk. <br \/><br \/>If you order A Vie-Galacia (to cut support), the Russian cannot hold Rumania against the attack, A Budapest-Rumania. <br \/><br \/>Please confirm we're on the same page. Thanks."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1285291,"phase":"S1906M","message":"Yes, those are my moves. I am moving on the Black Sea and Rum"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1287896,"phase":"S1906M","message":"Ok, thanks for confirming."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":1289051,"phase":"S1906M","message":"What are your plans with your northern fleets?"}]},{"name":"S1906R","state":{"timestamp":1537459327518825,"zobrist_hash":"4778202338952248162","note":"","name":"S1906R","units":{"AUSTRIA":["A VIE","A RUM","F BLA","A BUD"],"ENGLAND":["F NTH","F IRI"],"FRANCE":["A MUN","F CLY","A BEL","F MAO","A PIC","A HOL","F SPA\/SC","A BUR"],"GERMANY":["F DEN","A KIE","*F HOL"],"ITALY":["A SER","A TYR","A BUL","F ION","A ROM","F AEG","F CON","A GRE"],"RUSSIA":["A SEV","F NWY","A SWE","A GAL","A BER","F BAL","A SIL","*A RUM"],"TURKEY":["A ARM"]},"centers":{"AUSTRIA":["TRI","VIE","BUD","CON"],"ENGLAND":["EDI","LON"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN"],"GERMANY":["KIE","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","NWY","BER"],"TURKEY":["ANK"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","RUM","BLA","BUD"],"ENGLAND":["LON","YOR","NWG","ENG","EDI","NTH","IRI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","LVP","RUH","MUN","CLY","BEL","MAO","PIC","HOL","SPA","BUR"],"GERMANY":["DEN","SKA","BOH","BOT","KIE","HEL"],"ITALY":["NAP","ROM","TUN","VEN","APU","EAS","TRI","ALB","SER","TYR","BUL","TYS","SMY","ION","AEG","CON","GRE"],"RUSSIA":["MOS","STP","UKR","SEV","FIN","NWY","WAR","PRU","SWE","GAL","BER","BAL","SIL"],"TURKEY":["ANK","ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":["F HOL R HEL"],"ITALY":[],"RUSSIA":["A RUM R UKR"],"TURKEY":[]},"results":{"F HOL":[],"A RUM":[]},"messages":[{"sender":"ENGLAND","recipient":"ITALY","time_sent":1371250,"phase":"S1906R","message":"That's the wrong direction of France."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1376333,"phase":"S1906R","message":"I have some obligations to Austria to fulfill. Please be patient."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1376379,"phase":"S1906R","message":"Good guesses on your moves, btw."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1377191,"phase":"S1906R","message":"Two options: 1) I can order A Ser S Rumania and A Bul S Rumania. That will mean you keep Rumania and can make a supported attack on Galacia while I protect Rumania. Or, <br \/><br \/>2) you can support Rum with Bud and I will move Serbia-Greece. I propose we create a dmz in Serbia. Later, if in our best interests, we might decide to trade Serbia for Ankara maybe. <br \/><br \/>Obviously, the first option gives us a jump start on your attack of Russia, but the second makes our border stable. Please let me know your preference. <br \/><br \/>Also, I am ordering F Con S Black Sea to Ankara when Russia gets his retreat order finished."}]},{"name":"F1906M","state":{"timestamp":1537459327530647,"zobrist_hash":"4121050540296374117","note":"","name":"F1906M","units":{"AUSTRIA":["A VIE","A RUM","F BLA","A BUD"],"ENGLAND":["F NTH","F IRI"],"FRANCE":["A MUN","F CLY","A BEL","F MAO","A PIC","A HOL","F SPA\/SC","A BUR"],"GERMANY":["F DEN","A KIE","F HEL"],"ITALY":["A SER","A TYR","A BUL","F ION","A ROM","F AEG","F CON","A GRE"],"RUSSIA":["A SEV","F NWY","A SWE","A GAL","A BER","F BAL","A SIL","A UKR"],"TURKEY":["A ARM"]},"centers":{"AUSTRIA":["TRI","VIE","BUD","CON"],"ENGLAND":["EDI","LON"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN"],"GERMANY":["KIE","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","NWY","BER"],"TURKEY":["ANK"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","RUM","BLA","BUD"],"ENGLAND":["LON","YOR","NWG","ENG","EDI","NTH","IRI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","LVP","RUH","MUN","CLY","BEL","MAO","PIC","HOL","SPA","BUR"],"GERMANY":["DEN","SKA","BOH","BOT","KIE","HEL"],"ITALY":["NAP","ROM","TUN","VEN","APU","EAS","TRI","ALB","SER","TYR","BUL","TYS","SMY","ION","AEG","CON","GRE"],"RUSSIA":["MOS","STP","SEV","FIN","NWY","WAR","PRU","SWE","GAL","BER","BAL","SIL","UKR"],"TURKEY":["ANK","ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE - GAL","F BLA - ANK","A BUD S A RUM","A RUM S A BUD"],"ENGLAND":["F NTH - EDI","F IRI - ENG"],"FRANCE":["F CLY - LVP","A MUN H","A BEL S A HOL","F MAO - ENG","A HOL S A KIE","A PIC H","A BUR S A MUN","F SPA\/SC - MAO"],"GERMANY":["F DEN S F HEL - NTH","A KIE H","F HEL - NTH"],"ITALY":["A SER S A TYR - TRI","A TYR - TRI","A BUL S A SER","F ION - ADR","A ROM - TUS","A GRE - SMY VIA","F AEG C A GRE - SMY","F CON - ANK"],"RUSSIA":["A SEV H","F NWY H","A SWE - DEN","A GAL S A SIL - BOH","A SIL - BOH","F BAL S A SWE - DEN","A BER S A KIE","A UKR S A SEV"],"TURKEY":["A ARM H"]},"results":{"A VIE":["bounce"],"A RUM":[],"F BLA":["bounce"],"A BUD":[],"F NTH":[],"F IRI":["bounce"],"A MUN":[],"F CLY":[],"A BEL":[],"F MAO":["bounce"],"A PIC":[],"A HOL":[],"F SPA\/SC":["bounce"],"A BUR":[],"F DEN":["cut","dislodged"],"A KIE":[],"F HEL":[],"A SER":[],"A TYR":[],"A BUL":[],"F ION":[],"A ROM":[],"F AEG":[],"F CON":["bounce"],"A GRE":[],"A SEV":[],"F NWY":[],"A SWE":[],"A GAL":["cut"],"A BER":[],"F BAL":[],"A SIL":[],"A UKR":[],"A ARM":[]},"messages":[{"sender":"RUSSIA","recipient":"FRANCE","time_sent":1377679,"phase":"F1906M","message":"hi france. since we are getting close i thought we should start talking. i waont contest you for the brit island. <br \/>I do want to warn you about italy though. he says he will move on you soon. and he has broken every promise has made me for no reason. we are natural allies but he has fought me every turn. <br \/>so watch your back."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":1377724,"phase":"F1906M","message":"also, interested in a 2-way draw? we can easily work together without conpromising each other's interests"},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1378648,"phase":"F1906M","message":"I like the 2nd option."},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":1382010,"phase":"F1906M","message":"hey russia, it is good of you to warn me about italys intentions. I am a bit worried of course though we have been working well together until now. What about first of all taking out germany and england and then start talking about a draw? You take denmark, kiel is for me?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1382117,"phase":"F1906M","message":"Greetings from Russia (although without much love...). He informed me about your announcement to backstab me soon, just to let you know. I like our alliance and would really like to proceed with it until the end. Although we could actually think about taking out england, turkey, germany and austria and then draw between the three of us."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":1383058,"phase":"F1906M","message":"Thanks, I thought they'd try that."},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1383262,"phase":"F1906M","message":"Ok, we'll go with it then."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1384273,"phase":"F1906M","message":"He's lying. I never announced any plan to \"backstab\" you. <br \/><br \/>I admit I discussed the possibility that Russia and Italy might need to work together later in the game to stop you from a solo. However, as you can see by my moves, I am not worried about the threat of a French solo at this time. <br \/><br \/>I concur with your thoughts on our alliance. As long as we stick together, Russia is on the outs. Therefore, I'm sure Russia would like nothing better than to drive the wedge between us with propaganda."},{"sender":"AUSTRIA","recipient":"ITALY","time_sent":1386310,"phase":"F1906M","message":"Slow and steady always wins the race"},{"sender":"ITALY","recipient":"AUSTRIA","time_sent":1388088,"phase":"F1906M","message":"I don't think Russia will get two centers off Germany, maybe only one so he isn't going to build."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":1402994,"phase":"F1906M","message":"i know your in a position to take kiel, but can i have? i have a larger border to deal with and youre set"}]},{"name":"F1906R","state":{"timestamp":1537459327532829,"zobrist_hash":"4464118581213416417","note":"","name":"F1906R","units":{"AUSTRIA":["A VIE","A RUM","F BLA","A BUD"],"ENGLAND":["F IRI","F EDI"],"FRANCE":["A MUN","A BEL","F MAO","A PIC","A HOL","F SPA\/SC","A BUR","F LVP"],"GERMANY":["A KIE","F NTH","*F DEN"],"ITALY":["A SER","A BUL","F AEG","F CON","A TRI","F ADR","A TUS","A SMY"],"RUSSIA":["A SEV","F NWY","A GAL","A BER","F BAL","A UKR","A DEN","A BOH"],"TURKEY":["A ARM"]},"centers":{"AUSTRIA":["TRI","VIE","BUD","CON"],"ENGLAND":["EDI","LON"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN"],"GERMANY":["KIE","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY"],"RUSSIA":["MOS","SEV","STP","WAR","RUM","SWE","NWY","BER"],"TURKEY":["ANK"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","RUM","BLA","BUD"],"ENGLAND":["LON","YOR","NWG","ENG","IRI","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","RUH","MUN","CLY","BEL","MAO","PIC","HOL","SPA","BUR","LVP"],"GERMANY":["SKA","BOT","KIE","HEL","NTH"],"ITALY":["NAP","ROM","TUN","VEN","APU","EAS","ALB","SER","TYR","BUL","TYS","ION","AEG","CON","GRE","TRI","ADR","TUS","SMY"],"RUSSIA":["MOS","STP","SEV","FIN","NWY","WAR","PRU","SWE","GAL","BER","BAL","SIL","UKR","DEN","BOH"],"TURKEY":["ANK","ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":["F DEN R SKA"],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"F DEN":[]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":1463298,"phase":"F1906R","message":"Sorry, I didn't give you notice of the stab on Austria. I was afraid my move might get back to him. However, I am now ready for a full blown R\/I alliance when you are."}]},{"name":"W1906A","state":{"timestamp":1537459327534650,"zobrist_hash":"67545419202381552","note":"","name":"W1906A","units":{"AUSTRIA":["A VIE","A RUM","F BLA","A BUD"],"ENGLAND":["F IRI","F EDI"],"FRANCE":["A MUN","A BEL","F MAO","A PIC","A HOL","F SPA\/SC","A BUR","F LVP"],"GERMANY":["A KIE","F NTH","F SKA"],"ITALY":["A SER","A BUL","F AEG","F CON","A TRI","F ADR","A TUS","A SMY"],"RUSSIA":["A SEV","F NWY","A GAL","A BER","F BAL","A UKR","A DEN","A BOH"],"TURKEY":["A ARM"]},"centers":{"AUSTRIA":["VIE","BUD","RUM"],"ENGLAND":["EDI","LON"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL"],"GERMANY":["KIE"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON"],"RUSSIA":["MOS","SEV","STP","WAR","SWE","NWY","BER","DEN"],"TURKEY":["ANK"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","RUM","BLA","BUD"],"ENGLAND":["LON","YOR","NWG","ENG","IRI","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","RUH","MUN","CLY","BEL","MAO","PIC","HOL","SPA","BUR","LVP"],"GERMANY":["BOT","KIE","HEL","NTH","SKA"],"ITALY":["NAP","ROM","TUN","VEN","APU","EAS","ALB","SER","TYR","BUL","TYS","ION","AEG","CON","GRE","TRI","ADR","TUS","SMY"],"RUSSIA":["MOS","STP","SEV","FIN","NWY","WAR","PRU","SWE","GAL","BER","BAL","SIL","UKR","DEN","BOH"],"TURKEY":["ANK","ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":-1,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":1,"homes":["BRE","MAR","PAR"]},"GERMANY":{"count":-2,"homes":[]},"ITALY":{"count":2,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["F BLA D"],"ENGLAND":[],"FRANCE":["F BRE B"],"GERMANY":["A KIE D","F SKA D"],"ITALY":["F NAP B","A VEN B"],"RUSSIA":[],"TURKEY":[]},"results":{"F BLA":[""],"F BRE":[""],"A KIE":[""],"F SKA":[""],"F NAP":[""],"A VEN":[""]},"messages":[{"sender":"RUSSIA","recipient":"ITALY","time_sent":1548139,"phase":"W1906A","message":"I didnt need notice. but thanks"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1549964,"phase":"W1906A","message":"Well, the lack of notice meant that you didn't take Rumania back this fall. Should you accept an alliance, I would concede ownership to you of the centers in Budapest and Rumania. Hopefully, this is a start to a reconciliation and we can talk further of joint operations."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1558337,"phase":"W1906A","message":"I don't anticipate making much more gains against Russia until you start pressuring him in the north. However, I do think I can stalemate him with the position I am in now."}]},{"name":"S1907M","state":{"timestamp":1537459327545541,"zobrist_hash":"7823464526148874942","note":"","name":"S1907M","units":{"AUSTRIA":["A VIE","A RUM","A BUD"],"ENGLAND":["F IRI","F EDI"],"FRANCE":["A MUN","A BEL","F MAO","A PIC","A HOL","F SPA\/SC","A BUR","F LVP","F BRE"],"GERMANY":["F NTH"],"ITALY":["A SER","A BUL","F AEG","F CON","A TRI","F ADR","A TUS","A SMY","F NAP","A VEN"],"RUSSIA":["A SEV","F NWY","A GAL","A BER","F BAL","A UKR","A DEN","A BOH"],"TURKEY":["A ARM"]},"centers":{"AUSTRIA":["VIE","BUD","RUM"],"ENGLAND":["EDI","LON"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL"],"GERMANY":["KIE"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON"],"RUSSIA":["MOS","SEV","STP","WAR","SWE","NWY","BER","DEN"],"TURKEY":["ANK"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","RUM","BLA","BUD"],"ENGLAND":["LON","YOR","NWG","ENG","IRI","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","RUH","MUN","CLY","BEL","MAO","PIC","HOL","SPA","BUR","LVP"],"GERMANY":["BOT","KIE","HEL","NTH","SKA"],"ITALY":["NAP","ROM","TUN","VEN","APU","EAS","ALB","SER","TYR","BUL","TYS","ION","AEG","CON","GRE","TRI","ADR","TUS","SMY"],"RUSSIA":["MOS","STP","SEV","FIN","NWY","WAR","PRU","SWE","GAL","BER","BAL","SIL","UKR","DEN","BOH"],"TURKEY":["ANK","ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE S A BUD - TRI","A BUD - TRI","A RUM - SER"],"ENGLAND":["F IRI - WAL","F EDI - CLY"],"FRANCE":["A MUN H","A BEL - RUH","F MAO - ENG","A HOL S A PIC - BEL","A PIC - BEL","A BUR S A MUN","F SPA\/SC - MAO","F LVP H","F BRE S F MAO - ENG"],"GERMANY":["F NTH - LON"],"ITALY":["A SER S A RUM","A BUL S A SER","F AEG H","F CON - BLA","A TUS H","A SMY - ANK","F ADR S A TRI","A TRI S A VEN - TYR","F NAP - TYS","A VEN - TYR"],"RUSSIA":["A SEV - RUM","F NWY - NTH","A GAL S A BOH - VIE","F BAL - KIE","A BER S F BAL - KIE","A UKR S A SEV - RUM","A DEN S F BAL - KIE","A BOH - VIE"],"TURKEY":["A ARM H"]},"results":{"A VIE":["cut","dislodged"],"A RUM":["bounce","dislodged"],"A BUD":["bounce"],"F IRI":[],"F EDI":[],"A MUN":[],"A BEL":[],"F MAO":[],"A PIC":[],"A HOL":[],"F SPA\/SC":[],"A BUR":[],"F LVP":[],"F BRE":[],"F NTH":[],"A SER":["void"],"A BUL":[],"F AEG":[],"F CON":[],"A TRI":["cut"],"F ADR":[],"A TUS":[],"A SMY":[],"F NAP":[],"A VEN":[],"A SEV":[],"F NWY":[],"A GAL":[],"A BER":[],"F BAL":[],"A UKR":[],"A DEN":[],"A BOH":[],"A ARM":[]},"messages":[{"sender":"RUSSIA","recipient":"ITALY","time_sent":1586914,"phase":"S1907M","message":"why dont you take bud and ill take vie\/rum since that sets us up to grab them both this turn"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1618202,"phase":"S1907M","message":"I have no objection to your move. But I fell my defense is not strong enough yet to venture into Budapest."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1626329,"phase":"S1907M","message":"Ok. So, the silence is starting to concern me a bit."},{"sender":"FRANCE","recipient":"ITALY","time_sent":1636856,"phase":"S1907M","message":"Sorry, I am not at home at the moment, everything is fine with us, i contact you later"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1637322,"phase":"S1907M","message":"Ok. Great. See you on the upside. Good luck with your moves."}]},{"name":"F1907M","state":{"timestamp":1537459327558319,"zobrist_hash":"5648428834083982715","note":"","name":"F1907M","units":{"AUSTRIA":["A BUD"],"ENGLAND":["F WAL","F CLY"],"FRANCE":["A MUN","A HOL","A BUR","F LVP","F BRE","A RUH","F ENG","A BEL","F MAO"],"GERMANY":["F LON"],"ITALY":["A SER","A BUL","F AEG","A TRI","F ADR","A TUS","F BLA","A ANK","F TYS","A TYR"],"RUSSIA":["A GAL","A BER","A UKR","A DEN","A RUM","F NTH","F KIE","A VIE"],"TURKEY":["A ARM"]},"centers":{"AUSTRIA":["VIE","BUD","RUM"],"ENGLAND":["EDI","LON"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL"],"GERMANY":["KIE"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON"],"RUSSIA":["MOS","SEV","STP","WAR","SWE","NWY","BER","DEN"],"TURKEY":["ANK"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD"],"ENGLAND":["YOR","NWG","IRI","EDI","WAL","CLY"],"FRANCE":["PAR","POR","BRE","GAS","MAR","MUN","PIC","HOL","SPA","BUR","LVP","RUH","ENG","BEL","MAO"],"GERMANY":["BOT","HEL","SKA","LON"],"ITALY":["NAP","ROM","TUN","VEN","APU","EAS","ALB","SER","BUL","ION","AEG","CON","GRE","TRI","ADR","TUS","SMY","BLA","ANK","TYS","TYR"],"RUSSIA":["MOS","STP","SEV","FIN","NWY","WAR","PRU","SWE","GAL","BER","BAL","SIL","UKR","DEN","BOH","RUM","NTH","KIE","VIE"],"TURKEY":["ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD - TRI"],"ENGLAND":["F CLY S F WAL - LVP","F WAL - LVP"],"FRANCE":["A MUN - SIL","A HOL S A RUH - KIE","A BUR - MUN","F LVP - WAL","F BRE - MAO","A RUH - KIE","A BEL S A HOL","F ENG S F LVP - WAL","F MAO - IRI"],"GERMANY":["F LON H"],"ITALY":["A SER S A TRI - BUD","A BUL - RUM","F AEG - SMY","A TUS - VEN","F ADR H","A TRI - BUD","F TYS - ION","A TYR - VIE","F BLA S A UKR - SEV","A ANK - SMY"],"RUSSIA":["A GAL S A VIE","A BER S F KIE","A UKR - SEV","A DEN S F KIE","A RUM - SEV","F NTH S F LON","F KIE H","A VIE S A BUD - TRI"],"TURKEY":["A ARM - SMY"]},"results":{"A BUD":["bounce","dislodged"],"F WAL":["bounce"],"F CLY":[],"A MUN":[],"A HOL":[],"A BUR":[],"F LVP":["bounce"],"F BRE":[],"A RUH":["bounce"],"F ENG":[],"A BEL":[],"F MAO":[],"F LON":[],"A SER":[],"A BUL":["bounce"],"F AEG":["bounce"],"A TRI":[],"F ADR":[],"A TUS":[],"F BLA":[],"A ANK":["bounce"],"F TYS":[],"A TYR":["bounce"],"A GAL":[],"A BER":[],"A UKR":[],"A DEN":[],"A RUM":["bounce"],"F NTH":[],"F KIE":[],"A VIE":["cut"],"A ARM":["bounce"]},"messages":[{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":1736156,"phase":"F1907M","message":"i wont go for edin so you can combat france"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1749732,"phase":"F1907M","message":"Hello. My impression is that Russia is a bit of dot grabber. Strategically, it makes sense for him to not dot you in Edi, but I am not sure if he thinks like that."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":1751396,"phase":"F1907M","message":"Hello. My impression is that Russia is a bit of dot grabber. Strategically, it makes sense for him to not dot you in Edi, but I am not sure if he thinks like that."},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":1757204,"phase":"F1907M","message":"ill help you back to tri from buda"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":1766120,"phase":"F1907M","message":"Done"}]},{"name":"W1907A","state":{"timestamp":1537459327561762,"zobrist_hash":"3145900106128234727","note":"","name":"W1907A","units":{"AUSTRIA":[],"ENGLAND":["F WAL","F CLY"],"FRANCE":["A HOL","F LVP","A RUH","F ENG","A BEL","A SIL","A MUN","F MAO","F IRI"],"GERMANY":["F LON"],"ITALY":["A SER","A BUL","F AEG","F ADR","F BLA","A ANK","A TYR","A BUD","A VEN","F ION"],"RUSSIA":["A GAL","A BER","A DEN","A RUM","F NTH","F KIE","A VIE","A SEV"],"TURKEY":["A ARM"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL"],"GERMANY":["LON"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","BUD","ANK"],"RUSSIA":["MOS","SEV","STP","WAR","SWE","NWY","BER","DEN","VIE","RUM","KIE"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["YOR","NWG","EDI","WAL","CLY"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","SPA","BUR","LVP","RUH","ENG","BEL","SIL","MUN","MAO","IRI"],"GERMANY":["BOT","HEL","SKA","LON"],"ITALY":["NAP","ROM","TUN","APU","EAS","ALB","SER","BUL","AEG","CON","GRE","TRI","ADR","TUS","SMY","BLA","ANK","TYS","TYR","BUD","VEN","ION"],"RUSSIA":["MOS","STP","FIN","NWY","WAR","PRU","SWE","GAL","BER","BAL","UKR","DEN","BOH","RUM","NTH","KIE","VIE","SEV"],"TURKEY":["ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":-1,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":2,"homes":["NAP","ROM"]},"RUSSIA":{"count":3,"homes":["MOS","STP","WAR"]},"TURKEY":{"count":-1,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F WAL D"],"FRANCE":[],"GERMANY":[],"ITALY":["A NAP B","A ROM B"],"RUSSIA":["A MOS B","F STP\/NC B","A WAR B"],"TURKEY":["A ARM D"]},"results":{"F WAL":[""],"A NAP":[""],"A ROM":[""],"A MOS":[""],"F STP\/NC":[""],"A WAR":[""],"A ARM":[""],"A BUD":["disband"]},"messages":[{"sender":"ITALY","recipient":"FRANCE","time_sent":1817104,"phase":"W1907A","message":"Good move in Liverpool."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":1817151,"phase":"W1907A","message":"Good game Austria."},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":1818313,"phase":"W1907A","message":"Is this a joke?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1818675,"phase":"W1907A","message":"Thanks. You two are outgrowing me..."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":1819383,"phase":"W1907A","message":"No, sincerely."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1819454,"phase":"W1907A","message":"I think you're about to remedy the disparity."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":1819518,"phase":"W1907A","message":"You fought the good fight to the bitter end. I respect that. Moreover, you're a great ally."},{"sender":"RUSSIA","recipient":"ITALY","time_sent":1820712,"phase":"W1907A","message":"what wasnt"},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":1821367,"phase":"W1907A","message":"I don't blame Italy, you made the right diplomacy move. I was just never able to get a strong foothold."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1821521,"phase":"W1907A","message":"Vienna supports Budapest to Trieste, lol."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":1823945,"phase":"W1907A","message":"Yeah, the moves Budapest - Rumania and Vienna -Budapest while Russia moved to Galacia in Spring 01 made your job difficult from the outset."},{"sender":"FRANCE","recipient":"ITALY","time_sent":1872482,"phase":"W1907A","message":"well, with three russian builds we need to fasten up i'd say"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1882095,"phase":"W1907A","message":"Agreed. <br \/><br \/>FYI, I'm building two armies. Your move to Silesia should help you get Kiel\/Berlin and\/or loosen up the Russian defense about Vienna. <br \/><br \/>I have no idea what the German is doing. He seems bent on destroying England for some reason. Hopefully, he is not a toady for Russia."},{"sender":"FRANCE","recipient":"ITALY","time_sent":1882600,"phase":"W1907A","message":"hopefully not. I'll be at least able to take kiel before the russian armies arrive"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1888434,"phase":"W1907A","message":"You are guaranteed Berlin in the spring by striking at both of the Russian's German acquisitions. If you take Kiel, he will retreat the fleet to Baltic Sea where it is a much more useful unit."}]},{"name":"S1908M","state":{"timestamp":1537459327574722,"zobrist_hash":"6971855692642530665","note":"","name":"S1908M","units":{"AUSTRIA":[],"ENGLAND":["F CLY"],"FRANCE":["A HOL","F LVP","A RUH","F ENG","A BEL","A SIL","A MUN","F MAO","F IRI"],"GERMANY":["F LON"],"ITALY":["A SER","A BUL","F AEG","F ADR","F BLA","A ANK","A TYR","A BUD","A VEN","F ION","A NAP","A ROM"],"RUSSIA":["A GAL","A BER","A DEN","A RUM","F NTH","F KIE","A VIE","A SEV","A MOS","F STP\/NC","A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL"],"GERMANY":["LON"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","BUD","ANK"],"RUSSIA":["MOS","SEV","STP","WAR","SWE","NWY","BER","DEN","VIE","RUM","KIE"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["YOR","NWG","EDI","WAL","CLY"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","SPA","BUR","LVP","RUH","ENG","BEL","SIL","MUN","MAO","IRI"],"GERMANY":["BOT","HEL","SKA","LON"],"ITALY":["NAP","ROM","TUN","APU","EAS","ALB","SER","BUL","AEG","CON","GRE","TRI","ADR","TUS","SMY","BLA","ANK","TYS","TYR","BUD","VEN","ION"],"RUSSIA":["MOS","STP","FIN","NWY","WAR","PRU","SWE","GAL","BER","BAL","UKR","DEN","BOH","RUM","NTH","KIE","VIE","SEV"],"TURKEY":["ARM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F CLY - EDI"],"FRANCE":["A HOL S A RUH - KIE","F LVP - CLY","A RUH - KIE","A BEL S A HOL","F ENG H","A SIL - BER","F MAO S F ENG","A MUN S A SIL - BER","F IRI - NAO"],"GERMANY":["F LON H"],"ITALY":["A SER S A BUD - RUM","A BUL S A BUD - RUM","F AEG C A NAP - CON","F ADR H","A TYR - BOH","F BLA S A ANK - ARM","A ANK - ARM","A VEN - TYR","F ION C A NAP - CON","A BUD - RUM","A NAP - CON VIA","A ROM - VEN"],"RUSSIA":["A GAL - BUD","A BER S A DEN - KIE","A DEN - KIE","A RUM S A SEV","F NTH S F KIE - HOL","F KIE - HOL","A VIE S A GAL - BUD","A SEV S A RUM","A WAR - PRU","A MOS - WAR","F STP\/NC - NWY"],"TURKEY":[]},"results":{"F CLY":[],"A HOL":[],"F LVP":[],"A RUH":[],"F ENG":[],"A BEL":[],"A SIL":[],"A MUN":[],"F MAO":[],"F IRI":[],"F LON":[],"A SER":[],"A BUL":[],"F AEG":[],"F ADR":[],"F BLA":[],"A ANK":[],"A TYR":[],"A BUD":[],"A VEN":[],"F ION":[],"A NAP":[],"A ROM":[],"A GAL":[],"A BER":["cut","dislodged"],"A DEN":["bounce"],"A RUM":["cut","dislodged"],"F NTH":[],"F KIE":["bounce","dislodged"],"A VIE":[],"A SEV":[],"A MOS":[],"F STP\/NC":[],"A WAR":[]},"messages":[{"sender":"ITALY","recipient":"FRANCE","time_sent":1906609,"phase":"S1908M","message":"Hello. Are you planning to move to Bohemia this spring?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1978688,"phase":"S1908M","message":"no, sorry, I'll have to take berlin before he gets there."},{"sender":"ITALY","recipient":"FRANCE","time_sent":1982020,"phase":"S1908M","message":"I just wanted to make sure we didn't get in each other's way. I think I'll try for Bohemia. Good luck."}]},{"name":"S1908R","state":{"timestamp":1537459327577483,"zobrist_hash":"5580438968014885464","note":"","name":"S1908R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A HOL","F ENG","A BEL","A MUN","F MAO","F CLY","A KIE","A BER","F NAO"],"GERMANY":["F LON"],"ITALY":["A SER","A BUL","F AEG","F ADR","F BLA","F ION","A ARM","A BOH","A RUM","A TYR","A CON","A VEN"],"RUSSIA":["A DEN","F NTH","A VIE","A SEV","A BUD","A WAR","F NWY","A PRU","*A RUM","*F KIE"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL"],"GERMANY":["LON"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","BUD","ANK"],"RUSSIA":["MOS","SEV","STP","WAR","SWE","NWY","BER","DEN","VIE","RUM","KIE"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["YOR","NWG","WAL","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","SPA","BUR","LVP","RUH","ENG","BEL","SIL","MUN","MAO","IRI","CLY","KIE","BER","NAO"],"GERMANY":["BOT","HEL","SKA","LON"],"ITALY":["NAP","ROM","TUN","APU","EAS","ALB","SER","BUL","AEG","GRE","TRI","ADR","TUS","SMY","BLA","ANK","TYS","ION","ARM","BOH","RUM","TYR","CON","VEN"],"RUSSIA":["MOS","STP","FIN","SWE","GAL","BAL","UKR","DEN","NTH","VIE","SEV","BUD","WAR","NWY","PRU"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["A RUM R UKR","F KIE R HEL"],"TURKEY":[]},"results":{"A RUM":[],"F KIE":[],"A BER":["disband"]},"messages":[{"sender":"FRANCE","recipient":"ITALY","time_sent":1987334,"phase":"S1908R","message":"wow, that went better than expected. and you will be able to take vienna and retake budapest next turn"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1994347,"phase":"S1908R","message":"Congratulations on your success!"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1995301,"phase":"S1908R","message":"I'm still interested in an alliance with Russia. I am only fighting you because you are fighting me. If I tap Munich and don't try to take Vienna or Budapest from you, can we work out a deal? If I cut France's support from Munich, you retake Berlin and that last turn is not such a big disaster."}]},{"name":"F1908M","state":{"timestamp":1537459327590640,"zobrist_hash":"873349014993574751","note":"","name":"F1908M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A HOL","F ENG","A BEL","A MUN","F MAO","F CLY","A KIE","A BER","F NAO"],"GERMANY":["F LON"],"ITALY":["A SER","A BUL","F AEG","F ADR","F BLA","F ION","A ARM","A BOH","A RUM","A TYR","A CON","A VEN"],"RUSSIA":["A DEN","F NTH","A VIE","A SEV","A BUD","A WAR","F NWY","A PRU","A UKR","F HEL"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL"],"GERMANY":["LON"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","BUD","ANK"],"RUSSIA":["MOS","SEV","STP","WAR","SWE","NWY","BER","DEN","VIE","RUM","KIE"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["YOR","NWG","WAL","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","SPA","BUR","LVP","RUH","ENG","BEL","SIL","MUN","MAO","IRI","CLY","KIE","BER","NAO"],"GERMANY":["BOT","SKA","LON"],"ITALY":["NAP","ROM","TUN","APU","EAS","ALB","SER","BUL","AEG","GRE","TRI","ADR","TUS","SMY","BLA","ANK","TYS","ION","ARM","BOH","RUM","TYR","CON","VEN"],"RUSSIA":["MOS","STP","FIN","SWE","GAL","BAL","DEN","NTH","VIE","SEV","BUD","WAR","NWY","PRU","UKR","HEL"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A HOL S A KIE","A BEL S A HOL","F ENG - WAL","F MAO - ENG","A MUN S A KIE","F CLY S F NAO - NWG","A BER S A KIE","A KIE S A MUN","F NAO - NWG"],"GERMANY":["F LON H"],"ITALY":["A SER S A BUL - RUM","A BUL - RUM","F AEG - ION","F ADR S A VEN - TRI","F BLA C A CON - SEV","F ION - TUN","A CON - SEV VIA","A VEN - TRI","A TYR - VIE","A ARM S A CON - SEV","A BOH S A TYR - VIE","A RUM - UKR"],"RUSSIA":["A DEN - KIE","F NTH S F NWY - NWG","A VIE S A BUD","A SEV H","A PRU S A WAR - SIL","A WAR - SIL","A BUD S A VIE","F NWY - NWG","A UKR S A SEV","F HEL H"],"TURKEY":[]},"results":{"F EDI":[],"A HOL":[],"F ENG":[],"A BEL":[],"A MUN":[],"F MAO":[],"F CLY":[],"A KIE":["cut"],"A BER":[],"F NAO":["bounce"],"F LON":[],"A SER":[],"A BUL":["bounce"],"F AEG":[],"F ADR":[],"F BLA":[],"F ION":[],"A ARM":[],"A BOH":[],"A RUM":["bounce"],"A TYR":["bounce"],"A CON":[],"A VEN":[],"A DEN":["bounce"],"F NTH":[],"A VIE":["cut"],"A SEV":["dislodged"],"A BUD":[],"A WAR":[],"F NWY":["bounce"],"A PRU":[],"A UKR":["cut"],"F HEL":[]},"messages":[{"sender":"RUSSIA","recipient":"ITALY","time_sent":1997092,"phase":"F1908M","message":"why should i trust you? looks like you have a nice draw going with France"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":1998179,"phase":"F1908M","message":"ok. I understand. I need to show you some love."},{"sender":"FRANCE","recipient":"ITALY","time_sent":2000937,"phase":"F1908M","message":"Now we just have to finish this together and don't do anything foolish"},{"sender":"ITALY","recipient":"FRANCE","time_sent":2014467,"phase":"F1908M","message":"Agreed."},{"sender":"FRANCE","recipient":"ITALY","time_sent":2038458,"phase":"F1908M","message":"I just just really hope you don't decide to go against me now with your useless fleets in the med..."},{"sender":"ITALY","recipient":"FRANCE","time_sent":2050469,"phase":"F1908M","message":"I plan to honor our agreement."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2058948,"phase":"F1908M","message":"FYI, if you order A Budapest S Vienna you will retain Vienna and capture Budapest."}]},{"name":"F1908R","state":{"timestamp":1537459327593107,"zobrist_hash":"4758517394324170753","note":"","name":"F1908R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A HOL","A BEL","A MUN","F CLY","A KIE","A BER","F NAO","F WAL","F ENG"],"GERMANY":["F LON"],"ITALY":["A SER","A BUL","F ADR","F BLA","A ARM","A BOH","A RUM","A TYR","F ION","F TUN","A SEV","A TRI"],"RUSSIA":["A DEN","F NTH","A VIE","A BUD","F NWY","A PRU","A UKR","F HEL","A SIL","*A SEV"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL"],"GERMANY":["LON"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","BUD","ANK"],"RUSSIA":["MOS","SEV","STP","WAR","SWE","NWY","BER","DEN","VIE","RUM","KIE"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["YOR","NWG","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","SPA","BUR","LVP","RUH","BEL","MUN","MAO","IRI","CLY","KIE","BER","NAO","WAL","ENG"],"GERMANY":["BOT","SKA","LON"],"ITALY":["NAP","ROM","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","TUS","SMY","BLA","ANK","TYS","ARM","BOH","RUM","TYR","CON","VEN","ION","TUN","SEV","TRI"],"RUSSIA":["MOS","STP","FIN","SWE","GAL","BAL","DEN","NTH","VIE","BUD","WAR","NWY","PRU","UKR","HEL","SIL"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["A SEV R MOS"],"TURKEY":[]},"results":{"A SEV":[]},"messages":[{"sender":"ITALY","recipient":"GLOBAL","time_sent":2085034,"phase":"F1908R","message":"DATELINE: TUNESIA 1 Novembre, 1908<br \/><br \/>Italian sailors put into the port of Tunis for liberty of unspecified duration. The Italian sailors celebrated a long naval war against Austria and Turkey, now a prefect of the new Roman Empire. The French Mayor, Blaques Jaques Shellaques, the famous war hero, greeted the tired sailors with a speech. \"Zee French, are so hapee to see owaire Italian brozaires. Partay Hartay!\""}]},{"name":"W1908A","state":{"timestamp":1537459327594934,"zobrist_hash":"3840320941547295464","note":"","name":"W1908A","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A HOL","A BEL","A MUN","F CLY","A KIE","A BER","F NAO","F WAL","F ENG"],"GERMANY":["F LON"],"ITALY":["A SER","A BUL","F ADR","F BLA","A ARM","A BOH","A RUM","A TYR","F ION","F TUN","A SEV","A TRI"],"RUSSIA":["A DEN","F NTH","A VIE","A BUD","F NWY","A PRU","A UKR","F HEL","A SIL","A MOS"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE"],"GERMANY":["LON"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM"],"RUSSIA":["MOS","STP","WAR","SWE","NWY","DEN","VIE","BUD"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["YOR","NWG","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","SPA","BUR","LVP","RUH","BEL","MUN","MAO","IRI","CLY","KIE","BER","NAO","WAL","ENG"],"GERMANY":["BOT","SKA","LON"],"ITALY":["NAP","ROM","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","TUS","SMY","BLA","ANK","TYS","ARM","BOH","RUM","TYR","CON","VEN","ION","TUN","SEV","TRI"],"RUSSIA":["STP","FIN","SWE","GAL","BAL","DEN","NTH","VIE","BUD","WAR","NWY","PRU","UKR","HEL","SIL","MOS"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":2,"homes":["BRE","MAR","PAR"]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":1,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":-2,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["F BRE B","F MAR B"],"GERMANY":[],"ITALY":["A VEN B"],"RUSSIA":["F NWY D","F HEL D"],"TURKEY":[]},"results":{"F BRE":[""],"F MAR":[""],"A VEN":[""],"F NWY":[""],"F HEL":[""]},"messages":[{"sender":"RUSSIA","recipient":"FRANCE","time_sent":2092507,"phase":"W1908A","message":"good luck against the italian solo"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2141814,"phase":"W1908A","message":"Vienna and Budapest are a lost cause. Disband those armies and I will help you recapture Berlin and Kiel."}]},{"name":"S1909M","state":{"timestamp":1537459327606239,"zobrist_hash":"9037862939058316962","note":"","name":"S1909M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A HOL","A BEL","A MUN","F CLY","A KIE","A BER","F NAO","F WAL","F ENG","F BRE","F MAR"],"GERMANY":["F LON"],"ITALY":["A SER","A BUL","F ADR","F BLA","A ARM","A BOH","A RUM","A TYR","F ION","F TUN","A SEV","A TRI","A VEN"],"RUSSIA":["A DEN","F NTH","A VIE","A BUD","A PRU","A UKR","A SIL","A MOS"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE"],"GERMANY":["LON"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM"],"RUSSIA":["MOS","STP","WAR","SWE","NWY","DEN","VIE","BUD"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["YOR","NWG","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","SPA","BUR","LVP","RUH","BEL","MUN","MAO","IRI","CLY","KIE","BER","NAO","WAL","ENG"],"GERMANY":["BOT","SKA","LON"],"ITALY":["NAP","ROM","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","TUS","SMY","BLA","ANK","TYS","ARM","BOH","RUM","TYR","CON","VEN","ION","TUN","SEV","TRI"],"RUSSIA":["STP","FIN","SWE","GAL","BAL","DEN","NTH","VIE","BUD","WAR","NWY","PRU","UKR","HEL","SIL","MOS"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI - CLY"],"FRANCE":["A HOL - KIE","A BEL - HOL","A MUN S A BER - SIL","F CLY S F NAO - NWG","A BER - SIL","A KIE - BER","F NAO - NWG","F WAL S F ENG - LON","F ENG - LON","F BRE - ENG","F MAR - SPA\/SC"],"GERMANY":["F LON H"],"ITALY":["A SER S A RUM - BUD","A BUL - RUM","F ADR S A TRI","F BLA S A ARM - SEV","A TYR S A VEN - PIE","A ARM - SEV","A BOH - GAL","A RUM - BUD","A TRI S A RUM - BUD","A SEV - UKR","F TUN - WES","F ION - TYS","A VEN - PIE"],"RUSSIA":["A DEN - KIE","F NTH - NWG","A VIE - GAL","A PRU - BER","A BUD - RUM","A UKR S A MOS - SEV","A SIL S A PRU - BER","A MOS - SEV"],"TURKEY":[]},"results":{"F EDI":["bounce"],"A HOL":["bounce"],"A BEL":["bounce"],"A MUN":[],"F CLY":["cut"],"A KIE":["bounce"],"A BER":[],"F NAO":["bounce"],"F WAL":[],"F ENG":[],"F BRE":[],"F MAR":[],"F LON":["dislodged"],"A SER":[],"A BUL":[],"F ADR":[],"F BLA":[],"A ARM":["bounce"],"A BOH":["bounce"],"A RUM":[],"A TYR":[],"F ION":[],"F TUN":[],"A SEV":["bounce"],"A TRI":[],"A VEN":[],"A DEN":["bounce"],"F NTH":["bounce"],"A VIE":["bounce"],"A BUD":["bounce","dislodged"],"A PRU":["bounce"],"A UKR":[],"A SIL":["cut","dislodged"],"A MOS":["bounce"]},"messages":[{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":2182731,"phase":"S1909M","message":"Anything you want my lone fleet to do?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2223033,"phase":"S1909M","message":"I'm taking a shot at Munich. You should be able to retake Berlin with the right moves. France cannot stop it."},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2233023,"phase":"S1909M","message":"Please let me know. Obviously, Italy's former arrangement with France is now dead."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2233066,"phase":"S1909M","message":"Please be France's poison pill."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":2235629,"phase":"S1909M","message":"What is it you'd like me to do?"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2237116,"phase":"S1909M","message":"Not much you can do except offer Russia support for F Nth to Nwg, I guess. <br \/><br \/>Russia isn't talking to me presently."},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":2242713,"phase":"S1909M","message":"maybe hit cly?"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":2242836,"phase":"S1909M","message":"bugger off."},{"sender":"ITALY","recipient":"FRANCE","time_sent":2256067,"phase":"S1909M","message":"So, it's on?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":2262894,"phase":"S1909M","message":"Yes it is!"}]},{"name":"S1909R","state":{"timestamp":1537459327609114,"zobrist_hash":"2560901375990595888","note":"","name":"S1909R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A HOL","A BEL","A MUN","F CLY","A KIE","F NAO","F WAL","A SIL","F LON","F ENG","F SPA\/SC"],"GERMANY":["*F LON"],"ITALY":["A SER","F ADR","F BLA","A ARM","A BOH","A TYR","A SEV","A TRI","A RUM","A BUD","F TYS","F WES","A PIE"],"RUSSIA":["A DEN","F NTH","A VIE","A PRU","A UKR","A MOS","*A SIL"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE"],"GERMANY":["LON"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM"],"RUSSIA":["MOS","STP","WAR","SWE","NWY","DEN","VIE","BUD"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["YOR","NWG","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","BUR","LVP","RUH","BEL","MUN","MAO","IRI","CLY","KIE","BER","NAO","WAL","SIL","LON","ENG","SPA"],"GERMANY":["BOT","SKA"],"ITALY":["NAP","ROM","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","TUS","SMY","BLA","ANK","ARM","BOH","TYR","CON","VEN","ION","TUN","SEV","TRI","RUM","BUD","TYS","WES","PIE"],"RUSSIA":["STP","FIN","SWE","GAL","BAL","DEN","NTH","VIE","WAR","NWY","PRU","UKR","HEL","MOS"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":["F LON R YOR"],"ITALY":[],"RUSSIA":["A SIL R WAR"],"TURKEY":[]},"results":{"F LON":[],"A SIL":[],"A BUD":["disband"]},"messages":[{"sender":"ITALY","recipient":"FRANCE","time_sent":2267219,"phase":"S1909R","message":"Alright!"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":2268481,"phase":"S1909R","message":"So far so good, but I'm not going to last beyond next year :("},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":2280172,"phase":"S1909R","message":"i know. we both wont :-("}]},{"name":"F1909M","state":{"timestamp":1537459327619778,"zobrist_hash":"2571939214255678846","note":"","name":"F1909M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A HOL","A BEL","A MUN","F CLY","A KIE","F NAO","F WAL","A SIL","F LON","F ENG","F SPA\/SC"],"GERMANY":["F YOR"],"ITALY":["A SER","F ADR","F BLA","A ARM","A BOH","A TYR","A SEV","A TRI","A RUM","A BUD","F TYS","F WES","A PIE"],"RUSSIA":["A DEN","F NTH","A VIE","A PRU","A UKR","A MOS","A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE"],"GERMANY":["LON"],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM"],"RUSSIA":["MOS","STP","WAR","SWE","NWY","DEN","VIE","BUD"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","BUR","LVP","RUH","BEL","MUN","MAO","IRI","CLY","KIE","BER","NAO","WAL","SIL","LON","ENG","SPA"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","ROM","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","TUS","SMY","BLA","ANK","ARM","BOH","TYR","CON","VEN","ION","TUN","SEV","TRI","RUM","BUD","TYS","WES","PIE"],"RUSSIA":["STP","FIN","SWE","GAL","BAL","DEN","NTH","VIE","NWY","PRU","UKR","HEL","MOS","WAR"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI - CLY"],"FRANCE":["A HOL H","A BEL - BUR","A MUN H","F CLY H","A KIE S A MUN","F NAO H","F WAL - ENG","A SIL - BER","F ENG - MAO","F SPA\/SC H","F LON H"],"GERMANY":["F YOR - NTH"],"ITALY":["A SER - BUD","F ADR - ION","F BLA S A ARM - SEV","A TYR H","A ARM - SEV","A BOH - GAL","A TRI S A BUD - VIE","A SEV - UKR","F WES - MAO","A PIE S A TYR","A RUM S A BOH - GAL","A BUD - VIE","F TYS - LYO"],"RUSSIA":["A DEN H","F NTH H","A VIE - GAL","A PRU H","A UKR S A VIE - GAL","A MOS S A UKR","A WAR S A VIE - GAL"],"TURKEY":[]},"results":{"F EDI":["bounce"],"A HOL":[],"A BEL":[],"A MUN":[],"F CLY":[],"A KIE":[],"F NAO":[],"F WAL":["bounce"],"A SIL":[],"F LON":[],"F ENG":["bounce"],"F SPA\/SC":[],"F YOR":["bounce"],"A SER":[],"F ADR":[],"F BLA":[],"A ARM":["bounce"],"A BOH":["bounce"],"A TYR":[],"A SEV":["bounce"],"A TRI":[],"A RUM":[],"A BUD":[],"F TYS":[],"F WES":["bounce"],"A PIE":[],"A DEN":[],"F NTH":[],"A VIE":["bounce","dislodged"],"A PRU":[],"A UKR":["cut"],"A MOS":[],"A WAR":[]},"messages":[{"sender":"FRANCE","recipient":"ITALY","time_sent":2297809,"phase":"F1909M","message":"Well, now eventually you turn against me..."},{"sender":"ITALY","recipient":"FRANCE","time_sent":2308696,"phase":"F1909M","message":"Lol! I thought you said it was on."},{"sender":"FRANCE","recipient":"ITALY","time_sent":2310862,"phase":"F1909M","message":"Ah, I thought what you meant was our alliance is on. excuse my english. I just do not sea why we should fight each after working together so well. I have no problem with you having the honour of the victory while i get my credits for being second."},{"sender":"ITALY","recipient":"FRANCE","time_sent":2315961,"phase":"F1909M","message":"My bad. Your English is excellent. I had no idea it was not your first language! <br \/><br \/>I assumed after you built the fleet in Marseilles we were going to cross swords. I wrote that message with the idea that we should both understand this and execute our tactics, fighting a noble war until the draw is voted in."},{"sender":"ITALY","recipient":"GLOBAL","time_sent":2316367,"phase":"F1909M","message":"Sa·cré bleu. Zee Aynglays ees zee poizen peel!"},{"sender":"AUSTRIA","recipient":"GLOBAL","time_sent":2316508,"phase":"F1909M","message":"1909 and only two players are eliminated. Impressive."},{"sender":"FRANCE","recipient":"ITALY","time_sent":2317267,"phase":"F1909M","message":"well as far as i am concerned i could resist the noble war and just finish off england and germany and russia until the first of us has won enough supply centers. if we start now fighting each other russia will have all the profit from it"},{"sender":"ITALY","recipient":"FRANCE","time_sent":2328116,"phase":"F1909M","message":"I'm tempted to lie and try to deceive you but it doesn't feel right. <br \/><br \/>Let's take a whack at each other this fall and see what happens. If it appears as you say, then we can withdraw from each other and vote for the draw."},{"sender":"FRANCE","recipient":"ITALY","time_sent":2330505,"phase":"F1909M","message":"well, fair enough to say so. good luck!"},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":2330579,"phase":"F1909M","message":"since italy has eventually broken our alliance maybe this is now the time to come to terms?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":2333967,"phase":"F1909M","message":"Same to you!"},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":2336653,"phase":"F1909M","message":"stay out the NS and NorS and we can leave the borders as is and ill be ok."},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":2345704,"phase":"F1909M","message":"that sounds like a deal to me."}]},{"name":"W1909A","state":{"timestamp":1537459327622801,"zobrist_hash":"8396137093974117540","note":"","name":"W1909A","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A HOL","A MUN","F CLY","A KIE","F NAO","F WAL","F LON","F ENG","F SPA\/SC","A BUR","A BER"],"GERMANY":["F YOR"],"ITALY":["F BLA","A ARM","A BOH","A TYR","A SEV","A TRI","A RUM","F WES","A PIE","A BUD","F ION","A VIE","F LYO"],"RUSSIA":["A DEN","F NTH","A PRU","A UKR","A MOS","A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD"],"RUSSIA":["MOS","STP","WAR","SWE","NWY","DEN"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","LVP","RUH","BEL","MUN","MAO","IRI","CLY","KIE","NAO","WAL","SIL","LON","ENG","SPA","BUR","BER"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","ROM","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","TUS","SMY","BLA","ANK","ARM","BOH","TYR","CON","VEN","TUN","SEV","TRI","RUM","TYS","WES","PIE","BUD","ION","VIE","LYO"],"RUSSIA":["STP","FIN","SWE","GAL","BAL","DEN","NTH","NWY","PRU","UKR","HEL","MOS","WAR"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":1,"homes":["BRE","MAR","PAR"]},"GERMANY":{"count":-1,"homes":[]},"ITALY":{"count":2,"homes":["NAP","ROM","VEN"]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["F MAR B"],"GERMANY":["F YOR D"],"ITALY":["F NAP B","A VEN B"],"RUSSIA":[],"TURKEY":[]},"results":{"F MAR":[""],"F YOR":[""],"F NAP":[""],"A VEN":[""],"A VIE":["disband"]},"messages":[{"sender":"GERMANY","recipient":"GLOBAL","time_sent":2410381,"phase":"W1909A","message":"Three"},{"sender":"ITALY","recipient":"GLOBAL","time_sent":2411129,"phase":"W1909A","message":"Good game."}]},{"name":"S1910M","state":{"timestamp":1537459327634025,"zobrist_hash":"3292621779240123134","note":"","name":"S1910M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A HOL","A MUN","F CLY","A KIE","F NAO","F WAL","F LON","F ENG","F SPA\/SC","A BUR","A BER","F MAR"],"GERMANY":[],"ITALY":["F BLA","A ARM","A BOH","A TYR","A SEV","A TRI","A RUM","F WES","A PIE","A BUD","F ION","A VIE","F LYO","F NAP","A VEN"],"RUSSIA":["A DEN","F NTH","A PRU","A UKR","A MOS","A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD"],"RUSSIA":["MOS","STP","WAR","SWE","NWY","DEN"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","EDI"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","LVP","RUH","BEL","MUN","MAO","IRI","CLY","KIE","NAO","WAL","SIL","LON","ENG","SPA","BUR","BER"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","ROM","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","TUS","SMY","BLA","ANK","ARM","BOH","TYR","CON","VEN","TUN","SEV","TRI","RUM","TYS","WES","PIE","BUD","ION","VIE","LYO"],"RUSSIA":["STP","FIN","SWE","GAL","BAL","DEN","NTH","NWY","PRU","UKR","HEL","MOS","WAR"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI - CLY"],"FRANCE":["A HOL - BEL","A MUN H","F CLY S F NTH - EDI","A KIE S A MUN","F NAO - MAO","F WAL - IRI","F ENG S F NAO - MAO","F SPA\/SC S F MAR - LYO","F LON H","A BER H","A BUR - MAR","F MAR - LYO"],"GERMANY":[],"ITALY":["F BLA S A ARM - SEV","A TYR - BOH","A ARM - SEV","A BOH - SIL","A TRI - TYR","A SEV - UKR","F WES - MAO","A PIE - MAR","A RUM S A BUD - GAL","A BUD - GAL","F LYO - SPA\/SC","F ION - TUN","A VIE S A TYR - BOH","F NAP - ROM","A VEN - TUS"],"RUSSIA":["A DEN H","F NTH - EDI","A PRU - WAR","A UKR S A WAR - GAL","A MOS S A UKR","A WAR - GAL"],"TURKEY":[]},"results":{"F EDI":["bounce","dislodged"],"A HOL":[],"A MUN":[],"F CLY":[],"A KIE":[],"F NAO":[],"F WAL":[],"F LON":[],"F ENG":[],"F SPA\/SC":[],"A BUR":["bounce"],"A BER":[],"F MAR":[],"F BLA":[],"A ARM":["bounce"],"A BOH":[],"A TYR":[],"A SEV":["bounce"],"A TRI":[],"A RUM":[],"F WES":["bounce"],"A PIE":["bounce"],"A BUD":[],"F ION":[],"A VIE":[],"F LYO":["bounce","dislodged"],"F NAP":[],"A VEN":[],"A DEN":[],"F NTH":[],"A PRU":["bounce"],"A UKR":["cut"],"A MOS":[],"A WAR":["bounce"]},"messages":[{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":2432041,"phase":"S1910M","message":"Any request this turn? I imagine France will move Lon-Yor and also try for Norwegian, so I'm sure I'm dead after this year. Nothing we can really do about it, but if you want me to try something with you, I'll do it."},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":2437818,"phase":"S1910M","message":"can you sup me into edin? i need it more than you"},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":2492363,"phase":"S1910M","message":"yeah, okay, i can't take it anyway"},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":2493166,"phase":"S1910M","message":"thanks"},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":2493178,"phase":"S1910M","message":"maybe we can work together to push italy into a draw"},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":2497525,"phase":"S1910M","message":"i hope so!"}]},{"name":"S1910R","state":{"timestamp":1537459327637544,"zobrist_hash":"1980501095668194989","note":"","name":"S1910R","units":{"AUSTRIA":[],"ENGLAND":["*F EDI"],"FRANCE":["A MUN","F CLY","A KIE","F LON","F ENG","F SPA\/SC","A BUR","A BER","A BEL","F MAO","F IRI","F LYO"],"GERMANY":[],"ITALY":["F BLA","A ARM","A SEV","A RUM","F WES","A PIE","A VIE","A SIL","A BOH","A TYR","A GAL","F TUN","F ROM","A TUS","*F LYO"],"RUSSIA":["A DEN","A PRU","A UKR","A MOS","A WAR","F EDI"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD"],"RUSSIA":["MOS","STP","WAR","SWE","NWY","DEN"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","LVP","RUH","MUN","CLY","KIE","NAO","WAL","LON","ENG","SPA","BUR","BER","BEL","MAO","IRI","LYO"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","CON","VEN","SEV","TRI","RUM","TYS","WES","PIE","BUD","ION","VIE","SIL","BOH","TYR","GAL","TUN","ROM","TUS"],"RUSSIA":["STP","FIN","SWE","BAL","DEN","NTH","NWY","PRU","UKR","HEL","MOS","WAR","EDI"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI R NWG"],"FRANCE":[],"GERMANY":[],"ITALY":["F LYO R TYS"],"RUSSIA":[],"TURKEY":[]},"results":{"F EDI":[],"F LYO":[]},"messages":[{"sender":"FRANCE","recipient":"ITALY","time_sent":2497585,"phase":"S1910R","message":"well, you still want to fight?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":2499354,"phase":"S1910R","message":"I could have just given you Edi to keep it from France."}]},{"name":"F1910M","state":{"timestamp":1537459327649846,"zobrist_hash":"3424168989818572966","note":"","name":"F1910M","units":{"AUSTRIA":[],"ENGLAND":["F NWG"],"FRANCE":["A MUN","F CLY","A KIE","F LON","F ENG","F SPA\/SC","A BUR","A BER","A BEL","F MAO","F IRI","F LYO"],"GERMANY":[],"ITALY":["F BLA","A ARM","A SEV","A RUM","F WES","A PIE","A VIE","A SIL","A BOH","A TYR","A GAL","F TUN","F ROM","A TUS","F TYS"],"RUSSIA":["A DEN","A PRU","A UKR","A MOS","A WAR","F EDI"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD"],"RUSSIA":["MOS","STP","WAR","SWE","NWY","DEN"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG"],"FRANCE":["PAR","POR","BRE","GAS","MAR","PIC","HOL","LVP","RUH","MUN","CLY","KIE","NAO","WAL","LON","ENG","SPA","BUR","BER","BEL","MAO","IRI","LYO"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","CON","VEN","SEV","TRI","RUM","WES","PIE","BUD","ION","VIE","SIL","BOH","TYR","GAL","TUN","ROM","TUS","TYS"],"RUSSIA":["STP","FIN","SWE","BAL","DEN","NTH","NWY","PRU","UKR","HEL","MOS","WAR","EDI"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NWG - NWY"],"FRANCE":["A MUN H","F CLY H","A KIE S A MUN","F ENG S F IRI - MAO","F SPA\/SC S F LYO","F LON H","A BER S A MUN","A BUR - MAR","F IRI - MAO","A BEL - BUR","F LYO S A BUR - MAR","F MAO - NAF"],"GERMANY":[],"ITALY":["F BLA S A ARM - SEV","A ARM - SEV","A SEV - UKR","F WES H","A PIE - MAR","A RUM S A VIE - GAL","A VIE - GAL","F ROM - TUS","A TUS - PIE","F TUN - ION","A BOH S A SIL","A SIL S A GAL - WAR","A TYR - MUN","A GAL - WAR","F TYS S F ROM - TUS"],"RUSSIA":["A DEN H","A PRU H","A UKR H","A MOS H","A WAR H","F EDI H"],"TURKEY":[]},"results":{"F NWG":[],"A MUN":[],"F CLY":[],"A KIE":[],"F LON":[],"F ENG":[],"F SPA\/SC":[],"A BUR":[],"A BER":[],"A BEL":[],"F MAO":[],"F IRI":[],"F LYO":[],"F BLA":[],"A ARM":["bounce"],"A SEV":["bounce"],"A RUM":[],"F WES":[],"A PIE":["bounce"],"A VIE":[],"A SIL":[],"A BOH":[],"A TYR":["bounce"],"A GAL":[],"F TUN":[],"F ROM":["bounce"],"A TUS":["bounce"],"F TYS":[],"A DEN":[],"A PRU":[],"A UKR":[],"A MOS":[],"A WAR":["dislodged"],"F EDI":[]},"messages":[{"sender":"ITALY","recipient":"FRANCE","time_sent":2501851,"phase":"F1910M","message":"Let's play out the fall turn. After that, I will probably suggest we vote in a draw. Good moves on your part. I was hoping you might support yourself to the North Sea."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2503232,"phase":"F1910M","message":"Hypothetically, if I take Moscow, I could be persuaded to support you to Stp. Or maybe vice versa?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":2505699,"phase":"F1910M","message":"Works for me. Russia blew it."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2508730,"phase":"F1910M","message":"I will probably have to wait for the Spring before I start cutting Russia up. Meanwhile, you have to old on in Nwy. <br \/><br \/>Amazing job of survival you have done. You are the best player in this game with the crap you have done ... beginning from Spring 01. I just wish I had been Germany. In any case, I would love for you to be the death of both F and R."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":2511957,"phase":"F1910M","message":"I'm trying :) I did try sticking it to France, Germany, and Russia, which was too much. But that's what I like trying as England. Didn't pan out as much this time with Russia sticking it back to me in Norway."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2515536,"phase":"F1910M","message":"The German player was naive. He wanted an alliance with Russia from the outset, which is stupid unless F\/E are attacking you. Eventually, G and R are going to fight. Once you made that aggressive S01 move against France and grabbed the channel, Germany should have jumped on board. That was bold and aggressive, but not reckless because any sensible German would have joined with you against France. I love the way you dictated the action. I'm booking it for future games as England against cautious F and G players."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":2517057,"phase":"F1910M","message":"Yeah, my conversations with them was weird, especially when they suggested I might also be France. I outright asked them to join me against France, and they declined, suggesting I was probably going to attack them. Oh well. At least it was fun surviving this long!"},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2519243,"phase":"F1910M","message":"Too bad for Russia that France decided defending my attack on Iberia was more important that killing you. Looks like you are going to make it now. What the hell? I bet France was supposed to cover Nwg, lol."}]},{"name":"F1910R","state":{"timestamp":1537459327652182,"zobrist_hash":"4679333646170125380","note":"","name":"F1910R","units":{"AUSTRIA":[],"ENGLAND":["F NWY"],"FRANCE":["A MUN","F CLY","A KIE","F LON","F ENG","F SPA\/SC","A BER","F LYO","A MAR","A BUR","F NAF","F MAO"],"GERMANY":[],"ITALY":["F BLA","A ARM","A SEV","A RUM","F WES","A PIE","A SIL","A BOH","A TYR","F ROM","A TUS","F TYS","A GAL","A WAR","F ION"],"RUSSIA":["A DEN","A PRU","A UKR","A MOS","F EDI","*A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD"],"RUSSIA":["MOS","STP","WAR","SWE","NWY","DEN"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","NWY"],"FRANCE":["PAR","POR","BRE","GAS","PIC","HOL","LVP","RUH","MUN","CLY","KIE","NAO","WAL","LON","ENG","SPA","BER","BEL","IRI","LYO","MAR","BUR","NAF","MAO"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","CON","VEN","SEV","TRI","RUM","WES","PIE","BUD","VIE","SIL","BOH","TYR","TUN","ROM","TUS","TYS","GAL","WAR","ION"],"RUSSIA":["STP","FIN","SWE","BAL","DEN","NTH","PRU","UKR","HEL","MOS","EDI"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["A WAR R LVN"],"TURKEY":[]},"results":{"A WAR":[]},"messages":[{"sender":"ITALY","recipient":"FRANCE","time_sent":2592079,"phase":"F1910R","message":"What's with Russia? What a dickhead!"},{"sender":"FRANCE","recipient":"ITALY","time_sent":2594426,"phase":"F1910R","message":"Dont know, his not moving really complicates my situation..."}]},{"name":"W1910A","state":{"timestamp":1537459327653685,"zobrist_hash":"607005582589379720","note":"","name":"W1910A","units":{"AUSTRIA":[],"ENGLAND":["F NWY"],"FRANCE":["A MUN","F CLY","A KIE","F LON","F ENG","F SPA\/SC","A BER","F LYO","A MAR","A BUR","F NAF","F MAO"],"GERMANY":[],"ITALY":["F BLA","A ARM","A SEV","A RUM","F WES","A PIE","A SIL","A BOH","A TYR","F ROM","A TUS","F TYS","A GAL","A WAR","F ION"],"RUSSIA":["A DEN","A PRU","A UKR","A MOS","F EDI","A LVN"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD","WAR"],"RUSSIA":["MOS","STP","SWE","DEN","EDI"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","NWY"],"FRANCE":["PAR","POR","BRE","GAS","PIC","HOL","LVP","RUH","MUN","CLY","KIE","NAO","WAL","LON","ENG","SPA","BER","BEL","IRI","LYO","MAR","BUR","NAF","MAO"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","CON","VEN","SEV","TRI","RUM","WES","PIE","BUD","VIE","SIL","BOH","TYR","TUN","ROM","TUS","TYS","GAL","WAR","ION"],"RUSSIA":["STP","FIN","SWE","BAL","DEN","NTH","PRU","UKR","HEL","MOS","EDI","LVN"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":1,"homes":["NAP","VEN"]},"RUSSIA":{"count":-1,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":["A VEN B"],"RUSSIA":["A PRU D"],"TURKEY":[]},"results":{"A VEN":[""],"A PRU":[""]},"messages":[]},{"name":"S1911M","state":{"timestamp":1537459327666928,"zobrist_hash":"2956621507181725496","note":"","name":"S1911M","units":{"AUSTRIA":[],"ENGLAND":["F NWY"],"FRANCE":["A MUN","F CLY","A KIE","F LON","F ENG","F SPA\/SC","A BER","F LYO","A MAR","A BUR","F NAF","F MAO"],"GERMANY":[],"ITALY":["F BLA","A ARM","A SEV","A RUM","F WES","A PIE","A SIL","A BOH","A TYR","F ROM","A TUS","F TYS","A GAL","A WAR","F ION","A VEN"],"RUSSIA":["A DEN","A UKR","A MOS","F EDI","A LVN"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD","WAR"],"RUSSIA":["MOS","STP","SWE","DEN","EDI"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","NWY"],"FRANCE":["PAR","POR","BRE","GAS","PIC","HOL","LVP","RUH","MUN","CLY","KIE","NAO","WAL","LON","ENG","SPA","BER","BEL","IRI","LYO","MAR","BUR","NAF","MAO"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","CON","VEN","SEV","TRI","RUM","WES","PIE","BUD","VIE","SIL","BOH","TYR","TUN","ROM","TUS","TYS","GAL","WAR","ION"],"RUSSIA":["STP","FIN","SWE","BAL","DEN","NTH","PRU","UKR","HEL","MOS","EDI","LVN"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NWY - STP\/NC"],"FRANCE":["A MUN S A BER - SIL","F CLY - NAO","A KIE S A MUN","F ENG - MAO","F SPA\/SC S F LYO","F LON - NTH","A BER - SIL","F LYO S F MAO - WES","A BUR S A MUN","A MAR - PIE","F MAO - WES","F NAF S F MAO - WES"],"GERMANY":[],"ITALY":["F BLA C A RUM - ARM","A ARM - RUM VIA","A SEV S A WAR - MOS","F WES - TUN","A PIE H","A RUM - UKR","F ROM H","A TUS S A PIE","A BOH - SIL","A SIL - PRU","A TYR - BOH","F TYS H","F ION S F WES - TUN","A GAL S A RUM - UKR","A WAR - MOS","A VEN - TYR"],"RUSSIA":["A DEN H","A UKR H","A MOS H","F EDI H","A LVN H"],"TURKEY":[]},"results":{"F NWY":[],"A MUN":[],"F CLY":[],"A KIE":[],"F LON":[],"F ENG":[],"F SPA\/SC":[],"A BER":[],"F LYO":[],"A MAR":["bounce"],"A BUR":[],"F NAF":[],"F MAO":[],"F BLA":["void"],"A ARM":["no convoy"],"A SEV":[],"A RUM":[],"F WES":[],"A PIE":[],"A SIL":[],"A BOH":["bounce"],"A TYR":["bounce"],"F ROM":[],"A TUS":[],"F TYS":[],"A GAL":[],"A WAR":[],"F ION":[],"A VEN":["bounce"],"A DEN":[],"A UKR":["dislodged"],"A MOS":["dislodged"],"F EDI":[],"A LVN":[]},"messages":[{"sender":"ITALY","recipient":"ENGLAND","time_sent":2655442,"phase":"S1911M","message":"Maybe now is the best time to try for Stp? FYI, I'm cutting Mos with Sev this turn."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":2661637,"phase":"S1911M","message":"I'll give it a shot."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2666884,"phase":"S1911M","message":"I'm making a desperate move to take Moscow. I think that F\/R are going to try and eliminate you. Hopefully, Russia continues to screw up his tactics and Moscow falls. If he tries to bounce you out of Stp, then I will make it."},{"sender":"ENGLAND","recipient":"ITALY","time_sent":2669232,"phase":"S1911M","message":"Sounds good!"}]},{"name":"S1911R","state":{"timestamp":1537459327669305,"zobrist_hash":"3596068640419667313","note":"","name":"S1911R","units":{"AUSTRIA":[],"ENGLAND":["F STP\/NC"],"FRANCE":["A MUN","A KIE","F SPA\/SC","F LYO","A MAR","A BUR","F NAF","F NAO","F NTH","F MAO","A SIL","F WES"],"GERMANY":[],"ITALY":["F BLA","A ARM","A SEV","A PIE","A BOH","A TYR","F ROM","A TUS","F TYS","A GAL","F ION","A VEN","A UKR","F TUN","A PRU","A MOS"],"RUSSIA":["A DEN","F EDI","A LVN","*A UKR"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD","WAR"],"RUSSIA":["MOS","STP","SWE","DEN","EDI"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","NWY","STP"],"FRANCE":["PAR","POR","BRE","GAS","PIC","HOL","LVP","RUH","MUN","CLY","KIE","WAL","LON","ENG","SPA","BER","BEL","IRI","LYO","MAR","BUR","NAF","NAO","NTH","MAO","SIL","WES"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","CON","VEN","SEV","TRI","RUM","PIE","BUD","VIE","BOH","TYR","ROM","TUS","TYS","GAL","WAR","ION","UKR","TUN","PRU","MOS"],"RUSSIA":["FIN","SWE","BAL","DEN","HEL","EDI","LVN"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["A UKR R WAR"],"TURKEY":[]},"results":{"A UKR":[],"A MOS":["disband"]},"messages":[{"sender":"ITALY","recipient":"ENGLAND","time_sent":2741888,"phase":"S1911R","message":"The Russian is a dweeb!<br \/><br \/>However, it worked out well for us. Now it seems to me that you just need to bounce the French from Nwy to keep him from building."}]},{"name":"F1911M","state":{"timestamp":1537459327681902,"zobrist_hash":"5962005484057371905","note":"","name":"F1911M","units":{"AUSTRIA":[],"ENGLAND":["F STP\/NC"],"FRANCE":["A MUN","A KIE","F SPA\/SC","F LYO","A MAR","A BUR","F NAF","F NAO","F NTH","F MAO","A SIL","F WES"],"GERMANY":[],"ITALY":["F BLA","A ARM","A SEV","A PIE","A BOH","A TYR","F ROM","A TUS","F TYS","A GAL","F ION","A VEN","A UKR","F TUN","A PRU","A MOS"],"RUSSIA":["A DEN","F EDI","A LVN","A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD","WAR"],"RUSSIA":["MOS","STP","SWE","DEN","EDI"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","NWY","STP"],"FRANCE":["PAR","POR","BRE","GAS","PIC","HOL","LVP","RUH","MUN","CLY","KIE","WAL","LON","ENG","SPA","BER","BEL","IRI","LYO","MAR","BUR","NAF","NAO","NTH","MAO","SIL","WES"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","CON","VEN","SEV","TRI","RUM","PIE","BUD","VIE","BOH","TYR","ROM","TUS","TYS","GAL","ION","UKR","TUN","PRU","MOS"],"RUSSIA":["FIN","SWE","BAL","DEN","HEL","EDI","LVN","WAR"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F STP\/NC - NWY"],"FRANCE":["A MUN S A SIL - BER","A KIE - DEN","F SPA\/SC S F WES","F LYO S A MAR - PIE","A BUR S A MUN","A MAR - PIE","F NAF - TUN","F NAO - CLY","F NTH S A KIE - DEN","A SIL - BER","F MAO H","F WES S F NAF - TUN"],"GERMANY":[],"ITALY":["F BLA C A ARM - RUM","A ARM - RUM VIA","A SEV H","A PIE H","F ROM S F TYS","A TUS S A PIE","A BOH - SIL","A TYR - MUN","F TYS H","F ION S F TUN","A GAL S A UKR - WAR","A VEN - TRI","A UKR - WAR","A MOS S A PRU - LVN","A PRU - LVN","F TUN H"],"RUSSIA":["A DEN H","F EDI H","A LVN - MOS","A WAR S A LVN - MOS"],"TURKEY":[]},"results":{"F STP\/NC":[],"A MUN":["cut"],"A KIE":[],"F SPA\/SC":[],"F LYO":[],"A MAR":["bounce"],"A BUR":[],"F NAF":["bounce"],"F NAO":[],"F NTH":[],"F MAO":[],"A SIL":[],"F WES":[],"F BLA":[],"A ARM":[],"A SEV":[],"A PIE":[],"A BOH":[],"A TYR":["bounce"],"F ROM":[],"A TUS":[],"F TYS":[],"A GAL":[],"F ION":[],"A VEN":[],"A UKR":[],"F TUN":[],"A PRU":[],"A MOS":[],"A DEN":["dislodged"],"F EDI":[],"A LVN":["bounce","dislodged"],"A WAR":["cut","dislodged"]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":2754278,"phase":"F1911M","message":"France pretty much screwed you by letting England into Norway. Why not just take Kiel?"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2762508,"phase":"F1911M","message":"Alternatively, if France is not willing to offer a convoy of A Den to Norway that should tell you something about his view of the Russian interests."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2774651,"phase":"F1911M","message":"My Argument on Why You Should Help Me to Solo<br \/><br \/>1) With sum of squares scoring you get 1 point for a draw. Negligible point advantage for England.<br \/><br \/>2) The big winner in a draw vote is France followed by Russia. France would achieve 60 points with a draw. If I solo, France gets zero. Thus, he wants a draw and a solo hurts him most.<br \/><br \/>3) Why should England want to hurt France? Because he had an opportunity to ally with you against the German, but chose instead to sail into Irish Sea and stab you for Liverpool. He did effectively stab you. I'm a firm believer in the philosophy that an ally who backstabs me should not profit from it.<br \/><br \/>So, are you persuaded?"},{"sender":"ENGLAND","recipient":"ITALY","time_sent":2794879,"phase":"F1911M","message":"I've been with you for a while now. That's fine by me."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2830448,"phase":"F1911M","message":"yay"}]},{"name":"F1911R","state":{"timestamp":1537459327684463,"zobrist_hash":"2937959342740627150","note":"","name":"F1911R","units":{"AUSTRIA":[],"ENGLAND":["F NWY"],"FRANCE":["A MUN","F SPA\/SC","F LYO","A MAR","A BUR","F NAF","F NTH","F MAO","F WES","A DEN","F CLY","A BER"],"GERMANY":[],"ITALY":["F BLA","A SEV","A PIE","A TYR","F ROM","A TUS","F TYS","A GAL","F ION","F TUN","A MOS","A RUM","A SIL","A TRI","A WAR","A LVN"],"RUSSIA":["F EDI","*A DEN","*A LVN","*A WAR"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD","WAR"],"RUSSIA":["MOS","STP","SWE","DEN","EDI"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","STP","NWY"],"FRANCE":["PAR","POR","BRE","GAS","PIC","HOL","LVP","RUH","MUN","KIE","WAL","LON","ENG","SPA","BEL","IRI","LYO","MAR","BUR","NAF","NAO","NTH","MAO","WES","DEN","CLY","BER"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","CON","VEN","SEV","PIE","BUD","VIE","BOH","TYR","ROM","TUS","TYS","GAL","ION","UKR","TUN","PRU","MOS","RUM","SIL","TRI","WAR","LVN"],"RUSSIA":["FIN","SWE","BAL","HEL","EDI"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["A DEN R SWE","A LVN R STP","A WAR R PRU"],"TURKEY":[]},"results":{"A DEN":[],"A LVN":[],"A WAR":[]},"messages":[]},{"name":"W1911A","state":{"timestamp":1537459327686160,"zobrist_hash":"8120713091910460285","note":"","name":"W1911A","units":{"AUSTRIA":[],"ENGLAND":["F NWY"],"FRANCE":["A MUN","F SPA\/SC","F LYO","A MAR","A BUR","F NAF","F NTH","F MAO","F WES","A DEN","F CLY","A BER"],"GERMANY":[],"ITALY":["F BLA","A SEV","A PIE","A TYR","F ROM","A TUS","F TYS","A GAL","F ION","F TUN","A MOS","A RUM","A SIL","A TRI","A WAR","A LVN"],"RUSSIA":["F EDI","A SWE","A STP","A PRU"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON","DEN"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD","WAR","MOS"],"RUSSIA":["STP","SWE","EDI"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","NWY"],"FRANCE":["PAR","POR","BRE","GAS","PIC","HOL","LVP","RUH","MUN","KIE","WAL","LON","ENG","SPA","BEL","IRI","LYO","MAR","BUR","NAF","NAO","NTH","MAO","WES","DEN","CLY","BER"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","CON","VEN","SEV","PIE","BUD","VIE","BOH","TYR","ROM","TUS","TYS","GAL","ION","UKR","TUN","MOS","RUM","SIL","TRI","WAR","LVN"],"RUSSIA":["FIN","BAL","HEL","EDI","SWE","STP","PRU"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":1,"homes":["BRE","PAR"]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":1,"homes":["NAP","VEN"]},"RUSSIA":{"count":-1,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["A PAR B"],"GERMANY":[],"ITALY":["A VEN B"],"RUSSIA":["A PRU D"],"TURKEY":[]},"results":{"A PAR":[""],"A VEN":[""],"A PRU":[""]},"messages":[{"sender":"ENGLAND","recipient":"ITALY","time_sent":2838207,"phase":"W1911A","message":"well shoot, now they got me. good game!"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":2838270,"phase":"W1911A","message":"Too bad all that chasing of my little ship gave Italy the solo."},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":2838812,"phase":"W1911A","message":"I have no problems with him winning, I never had"}]},{"name":"S1912M","state":{"timestamp":1537459327698841,"zobrist_hash":"2734875001339813444","note":"","name":"S1912M","units":{"AUSTRIA":[],"ENGLAND":["F NWY"],"FRANCE":["A MUN","F SPA\/SC","F LYO","A MAR","A BUR","F NAF","F NTH","F MAO","F WES","A DEN","F CLY","A BER","A PAR"],"GERMANY":[],"ITALY":["F BLA","A SEV","A PIE","A TYR","F ROM","A TUS","F TYS","A GAL","F ION","F TUN","A MOS","A RUM","A SIL","A TRI","A WAR","A LVN","A VEN"],"RUSSIA":["F EDI","A SWE","A STP"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON","DEN"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD","WAR","MOS"],"RUSSIA":["STP","SWE","EDI"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","NWY"],"FRANCE":["PAR","POR","BRE","GAS","PIC","HOL","LVP","RUH","MUN","KIE","WAL","LON","ENG","SPA","BEL","IRI","LYO","MAR","BUR","NAF","NAO","NTH","MAO","WES","DEN","CLY","BER"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","CON","VEN","SEV","PIE","BUD","VIE","BOH","TYR","ROM","TUS","TYS","GAL","ION","UKR","TUN","MOS","RUM","SIL","TRI","WAR","LVN"],"RUSSIA":["FIN","BAL","HEL","EDI","SWE","STP","PRU"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NWY - SWE"],"FRANCE":["A MUN - KIE","F SPA\/SC S F WES","F LYO H","A BUR - MUN","A MAR H","F NAF H","F NTH S F CLY - EDI","F MAO - ENG","F WES H","A DEN H","A BER S A BUR - MUN","F CLY - EDI","A PAR - BUR"],"GERMANY":[],"ITALY":["F BLA - CON","A SEV - UKR","A PIE H","F ROM S F TYS","A TUS S A PIE","A TYR - BOH","F TYS H","F ION S F TUN","A GAL S A SIL","A MOS S A LVN - STP","F TUN H","A TRI S A VEN - TYR","A RUM - BUD","A WAR - PRU","A LVN - STP","A SIL S A TYR - BOH","A VEN - TYR"],"RUSSIA":["F EDI H","A STP H","A SWE H"],"TURKEY":[]},"results":{"F NWY":["bounce"],"A MUN":[],"F SPA\/SC":[],"F LYO":[],"A MAR":[],"A BUR":[],"F NAF":[],"F NTH":[],"F MAO":[],"F WES":[],"A DEN":[],"F CLY":[],"A BER":[],"A PAR":[],"F BLA":[],"A SEV":[],"A PIE":[],"A TYR":[],"F ROM":[],"A TUS":[],"F TYS":[],"A GAL":[],"F ION":[],"F TUN":[],"A MOS":[],"A RUM":[],"A SIL":[],"A TRI":[],"A WAR":[],"A LVN":[],"A VEN":[],"F EDI":["dislodged"],"A SWE":[],"A STP":["dislodged"]},"messages":[{"sender":"ITALY","recipient":"ENGLAND","time_sent":2839852,"phase":"S1912M","message":"I'm taking Stp so unless Russia supports Stp-Nwy, you should hold onto Nwy. Plus, I'll support you back into Nwy in the fall. Plus, Russia has a disband."},{"sender":"ITALY","recipient":"FRANCE","time_sent":2839991,"phase":"S1912M","message":"Good game. I don't like counting chickens before they hatch, and wouldn't be in position to win but for an idiotic Russian. However, you should support England to hold in Nwy just out of principle."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2840357,"phase":"S1912M","message":"You should offer to support Den-Sweden. I think France is fed up with his Russian ally."},{"sender":"ITALY","recipient":"ENGLAND","time_sent":2840577,"phase":"S1912M","message":"Finally, worst case, you get revenge because I win and France and Russia get no points."},{"sender":"FRANCE","recipient":"ITALY","time_sent":2842558,"phase":"S1912M","message":"you too, I am glad we didn't have to crush one another completely in the end."},{"sender":"ITALY","recipient":"FRANCE","time_sent":2850947,"phase":"S1912M","message":"You were pretty much crushing me in the Med for a while there, exhibiting good tactics."},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":2852422,"phase":"S1912M","message":"Just talking some smack :)"}]},{"name":"S1912R","state":{"timestamp":1537459327708206,"zobrist_hash":"5315979215384684414","note":"","name":"S1912R","units":{"AUSTRIA":[],"ENGLAND":["F NWY"],"FRANCE":["F SPA\/SC","F LYO","A MAR","F NAF","F NTH","F WES","A DEN","A BER","A KIE","A MUN","F ENG","F EDI","A BUR"],"GERMANY":[],"ITALY":["A PIE","F ROM","A TUS","F TYS","A GAL","F ION","F TUN","A MOS","A SIL","A TRI","F CON","A UKR","A BOH","A BUD","A PRU","A STP","A TYR"],"RUSSIA":["A SWE","*F EDI","*A STP"],"TURKEY":[]},"centers":{"AUSTRIA":[],"ENGLAND":["NWY"],"FRANCE":["MAR","PAR","POR","SPA","BRE","LVP","BEL","MUN","HOL","BER","KIE","LON","DEN"],"GERMANY":[],"ITALY":["NAP","ROM","VEN","TUN","SER","BUL","GRE","SMY","TRI","CON","ANK","SEV","RUM","VIE","BUD","WAR","MOS"],"RUSSIA":["STP","SWE","EDI"],"TURKEY":[]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["NWG","NWY"],"FRANCE":["PAR","POR","BRE","GAS","PIC","HOL","LVP","RUH","WAL","LON","SPA","BEL","IRI","LYO","MAR","NAF","NAO","NTH","MAO","WES","DEN","CLY","BER","KIE","MUN","ENG","EDI","BUR"],"GERMANY":["BOT","SKA","YOR"],"ITALY":["NAP","APU","EAS","ALB","SER","BUL","AEG","GRE","ADR","SMY","BLA","ANK","ARM","VEN","SEV","PIE","VIE","ROM","TUS","TYS","GAL","ION","TUN","MOS","RUM","SIL","TRI","WAR","LVN","CON","UKR","BOH","BUD","PRU","STP","TYR"],"RUSSIA":["FIN","BAL","HEL","SWE"],"TURKEY":[]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"001ce02c","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":null,"FRANCE":null,"GERMANY":[],"ITALY":null,"RUSSIA":null,"TURKEY":[]},"results":{},"messages":[]}]}
\ No newline at end of file diff --git a/diplomacy/tests/network/3.json b/diplomacy/tests/network/3.json new file mode 100644 index 0000000..c7041b0 --- /dev/null +++ b/diplomacy/tests/network/3.json @@ -0,0 +1 @@ +{"id":"0021f2cf","map":"standard","rules":[],"phases":[{"name":"S1901M","state":{"timestamp":1537459329362094,"zobrist_hash":"6621580922936090403","note":"","name":"S1901M","units":{"AUSTRIA":["A BUD","A VIE","F TRI"],"ENGLAND":["F EDI","F LON","A LVP"],"FRANCE":["F BRE","A MAR","A PAR"],"GERMANY":["F KIE","A BER","A MUN"],"ITALY":["F NAP","A ROM","A VEN"],"RUSSIA":["A WAR","A MOS","F SEV","F STP\/SC"],"TURKEY":["F ANK","A CON","A SMY"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BUD","VIE","TRI"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["KIE","BER","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["WAR","MOS","SEV","STP"],"TURKEY":["ANK","CON","SMY"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BUD - SER","A VIE - BUD","F TRI - VEN"],"ENGLAND":["F EDI - NWG","F LON - ENG","A LVP - EDI"],"FRANCE":["F BRE H","A MAR H","A PAR H"],"GERMANY":["F KIE - HOL","A BER - KIE","A MUN - RUH"],"ITALY":["F NAP - ION","A ROM - VEN","A VEN - TYR"],"RUSSIA":["A WAR - GAL","A MOS - UKR","F SEV - BLA","F STP\/SC - FIN"],"TURKEY":["F ANK - BLA","A CON - BUL","A SMY - CON"]},"results":{"A BUD":[],"A VIE":[],"F TRI":["bounce"],"F EDI":[],"F LON":[],"A LVP":[],"F BRE":[],"A MAR":[],"A PAR":[],"F KIE":[],"A BER":[],"A MUN":[],"F NAP":[],"A ROM":["bounce"],"A VEN":[],"A WAR":[],"A MOS":[],"F SEV":["bounce"],"F STP\/SC":[],"F ANK":["bounce"],"A CON":[],"A SMY":[]},"messages":[{"sender":"ENGLAND","recipient":"FRANCE","time_sent":0,"phase":"S1901M","message":"i hope u and i can work together"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":10,"phase":"S1901M","message":"Care to try the Juggernaut?"},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":23,"phase":"S1901M","message":"gl and SEMPER FI"},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":43,"phase":"S1901M","message":"Allies?"},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":77,"phase":"S1901M","message":"Wanna take down turkey?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":81,"phase":"S1901M","message":"definitely...Prussia and Silesia DMZ??"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":83,"phase":"S1901M","message":"Alliance? and together they go west?"},{"sender":"AUSTRIA","recipient":"RUSSIA","time_sent":107,"phase":"S1901M","message":"sure"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":143,"phase":"S1901M","message":"Aye the juggernaut its unstoppable"},{"sender":"RUSSIA","recipient":"AUSTRIA","time_sent":174,"phase":"S1901M","message":"Awesome"},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":177,"phase":"S1901M","message":"see u got it, OORAH"}]},{"name":"F1901M","state":{"timestamp":1537459329368973,"zobrist_hash":"207097916166081352","note":"","name":"F1901M","units":{"AUSTRIA":["F TRI","A SER","A BUD"],"ENGLAND":["F NWG","F ENG","A EDI"],"FRANCE":["F BRE","A MAR","A PAR"],"GERMANY":["F HOL","A KIE","A RUH"],"ITALY":["A ROM","F ION","A TYR"],"RUSSIA":["F SEV","A GAL","A UKR","F FIN"],"TURKEY":["F ANK","A BUL","A CON"]},"centers":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["VIE","TRI","SER","BUD"],"ENGLAND":["LON","LVP","NWG","ENG","EDI"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","MUN","HOL","KIE","RUH"],"ITALY":["NAP","ROM","VEN","ION","TYR"],"RUSSIA":["WAR","MOS","SEV","STP","GAL","UKR","FIN"],"TURKEY":["ANK","SMY","BUL","CON"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":["F TRI - VEN","A BUD - VIE","A SER H"],"ENGLAND":["F NWG C A EDI - NWY","A EDI - NWY VIA","F ENG H"],"FRANCE":["F BRE H","A MAR H","A PAR H"],"GERMANY":["F HOL S A RUH - BEL","A KIE - DEN","A RUH - BEL"],"ITALY":["A ROM - VEN","F ION - TUN","A TYR - PIE"],"RUSSIA":["F SEV S A UKR - RUM","A GAL - BUD","A UKR - RUM","F FIN - SWE"],"TURKEY":["F ANK - BLA","A BUL - GRE","A CON - BUL"]},"results":{"F TRI":["bounce"],"A SER":[],"A BUD":[],"F NWG":[],"F ENG":[],"A EDI":[],"F BRE":[],"A MAR":[],"A PAR":[],"F HOL":[],"A KIE":[],"A RUH":[],"A ROM":["bounce"],"F ION":[],"A TYR":[],"F SEV":[],"A GAL":[],"A UKR":[],"F FIN":[],"F ANK":[],"A BUL":[],"A CON":[]},"messages":[{"sender":"RUSSIA","recipient":"GERMANY","time_sent":212,"phase":"F1901M","message":"Agreed"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":239,"phase":"F1901M","message":"Alliance? and together they go west?"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":252,"phase":"F1901M","message":"nice"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":307,"phase":"F1901M","message":"nice :D"},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":343,"phase":"F1901M","message":"i think it is inevitable that the US will go to war with russia"},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":347,"phase":"F1901M","message":"Well as an army man I say it properly"},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":353,"phase":"F1901M","message":"in real life"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":383,"phase":"F1901M","message":"Hey dmz black sea"},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":424,"phase":"F1901M","message":"Most excellent sir"},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":427,"phase":"F1901M","message":"damn ruskies always piss me off, cant wait to fly a football field sized US flag in moscow"},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":467,"phase":"F1901M","message":"What are you talking about...."}]},{"name":"W1901A","state":{"timestamp":1537459329371868,"zobrist_hash":"8890435501224696475","note":"","name":"W1901A","units":{"AUSTRIA":["F TRI","A SER","A VIE"],"ENGLAND":["F NWG","F ENG","A NWY"],"FRANCE":["F BRE","A MAR","A PAR"],"GERMANY":["F HOL","A DEN","A BEL"],"ITALY":["A ROM","F TUN","A PIE"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE"],"TURKEY":["F BLA","A GRE","A BUL"]},"centers":{"AUSTRIA":["TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE"],"TURKEY":["ANK","CON","SMY","BUL","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI","SER","VIE"],"ENGLAND":["LON","LVP","NWG","ENG","EDI","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","MUN","HOL","KIE","RUH","DEN","BEL"],"ITALY":["NAP","ROM","VEN","ION","TYR","TUN","PIE"],"RUSSIA":["WAR","MOS","SEV","STP","GAL","UKR","FIN","BUD","RUM","SWE"],"TURKEY":["ANK","SMY","CON","BLA","GRE","BUL"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":1,"homes":["EDI","LON","LVP"]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":3,"homes":["BER","KIE","MUN"]},"ITALY":{"count":1,"homes":["NAP","VEN"]},"RUSSIA":{"count":3,"homes":["MOS","STP","WAR"]},"TURKEY":{"count":2,"homes":["ANK","CON","SMY"]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["A LON B"],"FRANCE":[],"GERMANY":["A BER B","F KIE B","A MUN B"],"ITALY":["F NAP B"],"RUSSIA":["A MOS B","A STP B","A WAR B"],"TURKEY":["A CON B","F SMY B"]},"results":{"A LON":[""],"A BER":[""],"F KIE":[""],"A MUN":[""],"F NAP":[""],"A MOS":[""],"A STP":[""],"A WAR":[""],"A CON":[""],"F SMY":[""]},"messages":[{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":494,"phase":"W1901A","message":"invading russia"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":505,"phase":"W1901A","message":"nice :D"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":591,"phase":"W1901A","message":"yes i think that this is going to be a very good relationship"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":603,"phase":"W1901A","message":"Uh........ why no dmz"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":625,"phase":"W1901A","message":"What is dmc? Idont understand! :S"},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":670,"phase":"W1901A","message":"Better watch those english they have asked me for help against you"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":720,"phase":"W1901A","message":"What is dmc? Idont understand! :S"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":734,"phase":"W1901A","message":"Demilitarized zone. No units go there its common in this alliance."},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":769,"phase":"W1901A","message":"yeah its okay if he is smart he is going to try and take advantage of france before i do and its not like he is going to get anywhere against me anyway"},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":775,"phase":"W1901A","message":"wats ur opinion on that?"},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":781,"phase":"W1901A","message":"I feel a bit uncomfortable now"}]},{"name":"S1902M","state":{"timestamp":1537459329381213,"zobrist_hash":"3918018667929359509","note":"","name":"S1902M","units":{"AUSTRIA":["F TRI","A SER","A VIE"],"ENGLAND":["F NWG","F ENG","A NWY","A LON"],"FRANCE":["F BRE","A MAR","A PAR"],"GERMANY":["F HOL","A DEN","A BEL","A BER","F KIE","A MUN"],"ITALY":["A ROM","F TUN","A PIE","F NAP"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A STP","A WAR"],"TURKEY":["F BLA","A GRE","A BUL","A CON","F SMY"]},"centers":{"AUSTRIA":["TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE"],"TURKEY":["ANK","CON","SMY","BUL","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI","SER","VIE"],"ENGLAND":["LON","LVP","NWG","ENG","EDI","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","MUN","HOL","KIE","RUH","DEN","BEL"],"ITALY":["NAP","ROM","VEN","ION","TYR","TUN","PIE"],"RUSSIA":["WAR","MOS","SEV","STP","GAL","UKR","FIN","BUD","RUM","SWE"],"TURKEY":["ANK","SMY","CON","BLA","GRE","BUL"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":["F TRI - VEN","A SER S A VIE - BUD","A VIE - BUD"],"ENGLAND":["F NWG S A NWY","F ENG - NTH","A NWY H","A LON H"],"FRANCE":["F BRE - MAO","A MAR - SPA","A PAR - BUR"],"GERMANY":["F HOL - BEL","A DEN H","A BEL - BUR","F KIE - HOL","A BER - KIE","A MUN - BUR"],"ITALY":["A ROM - VEN","F TUN - WES","A PIE S A ROM - VEN","F NAP - TYS"],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","F SWE S A STP - NWY","A BUD S A RUM","A WAR - GAL","A MOS - STP","A STP - NWY"],"TURKEY":["A GRE S A BUL - SER","A BUL - SER","F BLA H","A CON - BUL VIA","F SMY - AEG"]},"results":{"F TRI":["bounce"],"A SER":["cut","dislodged"],"A VIE":["bounce"],"F NWG":[],"F ENG":[],"A NWY":[],"A LON":[],"F BRE":[],"A MAR":[],"A PAR":["bounce"],"F HOL":["bounce"],"A DEN":[],"A BEL":["bounce"],"A BER":["bounce"],"F KIE":["bounce"],"A MUN":["bounce"],"A ROM":[],"F TUN":[],"A PIE":[],"F NAP":[],"F SEV":[],"A BUD":["cut"],"A RUM":[],"F SWE":[],"A MOS":["bounce"],"A STP":["bounce"],"A WAR":[],"F BLA":[],"A GRE":[],"A BUL":[],"A CON":[],"F SMY":[]},"messages":[{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":826,"phase":"S1902M","message":"y wats wrong"},{"sender":"RUSSIA","recipient":"GERMANY","time_sent":827,"phase":"S1902M","message":"Lol just saying mate"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":849,"phase":"S1902M","message":"Serbia and triest will be mine, and budapest and wien will be yours ok?"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":896,"phase":"S1902M","message":"hi i just joined do you want to work together?"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":906,"phase":"S1902M","message":"hey i am going to picardy is that ok"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":922,"phase":"S1902M","message":"yes"},{"sender":"ITALY","recipient":"TURKEY","time_sent":944,"phase":"S1902M","message":"Hey Turkey, i could not help but see that you built another fleet in smyrna and i was just wondering you plans with that fleet there??"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":946,"phase":"S1902M","message":"i see youre going against russia"},{"sender":"GERMANY","recipient":"RUSSIA","time_sent":964,"phase":"S1902M","message":"yepp i hear ya, thanks for the heads up"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":990,"phase":"S1902M","message":"hey dont attack me, i have no intent to attack u"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":995,"phase":"S1902M","message":"i was going to go to picardy and then i could support you into brest next turn"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":1000,"phase":"S1902M","message":"yup"},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":1030,"phase":"S1902M","message":"Well in game I'm russia......"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1070,"phase":"S1902M","message":"Ja"}]},{"name":"S1902R","state":{"timestamp":1537459329383321,"zobrist_hash":"7863535090478519912","note":"","name":"S1902R","units":{"AUSTRIA":["F TRI","A VIE","*A SER"],"ENGLAND":["F NWG","A NWY","A LON","F NTH"],"FRANCE":["A PAR","F MAO","A SPA"],"GERMANY":["F HOL","A DEN","A BEL","A BER","F KIE","A MUN"],"ITALY":["A PIE","A VEN","F WES","F TYS"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A STP","A GAL"],"TURKEY":["F BLA","A GRE","A SER","A BUL","F AEG"]},"centers":{"AUSTRIA":["TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE"],"TURKEY":["ANK","CON","SMY","BUL","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI","VIE"],"ENGLAND":["LON","LVP","NWG","ENG","EDI","NWY","NTH"],"FRANCE":["BRE","MAR","PAR","MAO","SPA"],"GERMANY":["BER","MUN","HOL","KIE","RUH","DEN","BEL"],"ITALY":["NAP","ROM","ION","TYR","TUN","PIE","VEN","WES","TYS"],"RUSSIA":["WAR","MOS","SEV","STP","UKR","FIN","BUD","RUM","SWE","GAL"],"TURKEY":["ANK","SMY","CON","BLA","GRE","SER","BUL","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":["A SER R ALB"],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A SER":[]},"messages":[{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":1102,"phase":"S1902R","message":"oh i see, i have been in the CORPS for two years now i love it"}]},{"name":"F1902M","state":{"timestamp":1537459329393697,"zobrist_hash":"2105046980571567013","note":"","name":"F1902M","units":{"AUSTRIA":["F TRI","A VIE","A ALB"],"ENGLAND":["F NWG","A NWY","A LON","F NTH"],"FRANCE":["A PAR","F MAO","A SPA"],"GERMANY":["F HOL","A DEN","A BEL","A BER","F KIE","A MUN"],"ITALY":["A PIE","A VEN","F WES","F TYS"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A STP","A GAL"],"TURKEY":["F BLA","A GRE","A SER","A BUL","F AEG"]},"centers":{"AUSTRIA":["TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE"],"TURKEY":["ANK","CON","SMY","BUL","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI","VIE","ALB"],"ENGLAND":["LON","LVP","NWG","ENG","EDI","NWY","NTH"],"FRANCE":["BRE","MAR","PAR","MAO","SPA"],"GERMANY":["BER","MUN","HOL","KIE","RUH","DEN","BEL"],"ITALY":["NAP","ROM","ION","TYR","TUN","PIE","VEN","WES","TYS"],"RUSSIA":["WAR","MOS","SEV","STP","UKR","FIN","BUD","RUM","SWE","GAL"],"TURKEY":["ANK","SMY","CON","BLA","GRE","SER","BUL","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":["F TRI H","A VIE H","A ALB H"],"ENGLAND":["F NWG - BAR","A NWY - STP","A LON H","F NTH S A NWY"],"FRANCE":["A PAR - PIC","F MAO - POR","A SPA S F MAO - POR"],"GERMANY":["F HOL - BEL","A DEN H","A BEL - PIC","F KIE - HEL","A BER - KIE","A MUN - RUH"],"ITALY":["A PIE - TYR","F WES - SPA\/SC","F TYS - ION","A VEN S A PIE - TYR"],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","F SWE S A STP - FIN","A BUD S A GAL - VIE","A MOS - STP","A STP - NWY","A GAL - VIE"],"TURKEY":["A GRE - ALB","F BLA H","A SER S A GRE - ALB","A BUL S A SER","F AEG - ION"]},"results":{"F TRI":[],"A VIE":["dislodged"],"A ALB":["dislodged"],"F NWG":[],"A NWY":["bounce"],"A LON":[],"F NTH":["void"],"A PAR":["bounce"],"F MAO":[],"A SPA":["cut"],"F HOL":["bounce"],"A DEN":[],"A BEL":["bounce"],"A BER":[],"F KIE":[],"A MUN":[],"A PIE":[],"A VEN":[],"F WES":["bounce"],"F TYS":["bounce"],"F SEV":[],"A BUD":[],"A RUM":[],"F SWE":["void"],"A MOS":["bounce"],"A STP":["bounce"],"A GAL":[],"F BLA":[],"A GRE":[],"A SER":[],"A BUL":[],"F AEG":["bounce"]},"messages":[{"sender":"FRANCE","recipient":"ITALY","time_sent":1138,"phase":"F1902M","message":"i hope we can work together"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1168,"phase":"F1902M","message":"as i see that austria is attacking you"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":1178,"phase":"F1902M","message":"Ok ill stop"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1201,"phase":"F1902M","message":"Where are you from in real?"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1206,"phase":"F1902M","message":"definitely if we stay away from each other i will move out of piedmont and western med if we can keep those DMZ"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":1275,"phase":"F1902M","message":"ok, so ur an army dog"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1341,"phase":"F1902M","message":"sure"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1347,"phase":"F1902M","message":"Hey bounce trieste with serbia so I can get vienna"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1357,"phase":"F1902M","message":"so that just completely messed up my last turns moves"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":1398,"phase":"F1902M","message":"Aye I get commissioned a us army officer afgter college"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":1414,"phase":"F1902M","message":"my bad, i will support u if i can but i am going to war sorry"}]},{"name":"F1902R","state":{"timestamp":1537459329395720,"zobrist_hash":"5448110353009076591","note":"","name":"F1902R","units":{"AUSTRIA":["F TRI","*A VIE"],"ENGLAND":["A NWY","A LON","F NTH","F BAR"],"FRANCE":["A PAR","A SPA","F POR"],"GERMANY":["F HOL","A DEN","A BEL","A KIE","F HEL","A RUH"],"ITALY":["A VEN","F WES","F TYS","A TYR"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A STP","A VIE"],"TURKEY":["F BLA","A SER","A BUL","F AEG","A ALB"]},"centers":{"AUSTRIA":["TRI","VIE","SER"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE"],"TURKEY":["ANK","CON","SMY","BUL","GRE"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI"],"ENGLAND":["LON","LVP","NWG","ENG","EDI","NWY","NTH","BAR"],"FRANCE":["BRE","MAR","PAR","MAO","SPA","POR"],"GERMANY":["BER","MUN","HOL","DEN","BEL","KIE","HEL","RUH"],"ITALY":["NAP","ROM","ION","TUN","PIE","VEN","WES","TYS","TYR"],"RUSSIA":["WAR","MOS","SEV","STP","UKR","FIN","BUD","RUM","SWE","GAL","VIE"],"TURKEY":["ANK","SMY","CON","BLA","GRE","SER","BUL","AEG","ALB"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":["A VIE R BOH"],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A VIE":[],"A ALB":["disband"]},"messages":[{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1440,"phase":"F1902R","message":"Where are you from in real?"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1458,"phase":"F1902R","message":"no its okay i got it"}]},{"name":"W1902A","state":{"timestamp":1537459329397306,"zobrist_hash":"2981714804210644495","note":"","name":"W1902A","units":{"AUSTRIA":["F TRI","A BOH"],"ENGLAND":["A NWY","A LON","F NTH","F BAR"],"FRANCE":["A PAR","A SPA","F POR"],"GERMANY":["F HOL","A DEN","A BEL","A KIE","F HEL","A RUH"],"ITALY":["A VEN","F WES","F TYS","A TYR"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A STP","A VIE"],"TURKEY":["F BLA","A SER","A BUL","F AEG","A ALB"]},"centers":{"AUSTRIA":["TRI"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI","BOH"],"ENGLAND":["LON","LVP","NWG","ENG","EDI","NWY","NTH","BAR"],"FRANCE":["BRE","MAR","PAR","MAO","SPA","POR"],"GERMANY":["BER","MUN","HOL","DEN","BEL","KIE","HEL","RUH"],"ITALY":["NAP","ROM","ION","TUN","PIE","VEN","WES","TYS","TYR"],"RUSSIA":["WAR","MOS","SEV","STP","UKR","FIN","BUD","RUM","SWE","GAL","VIE"],"TURKEY":["ANK","SMY","CON","BLA","GRE","SER","BUL","AEG","ALB"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":-1,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":2,"homes":["BRE","MAR"]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":1,"homes":["WAR"]},"TURKEY":{"count":1,"homes":["ANK","CON","SMY"]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":["F TRI D"],"ENGLAND":[],"FRANCE":["A BRE B","A MAR B"],"GERMANY":[],"ITALY":[],"RUSSIA":["A WAR B"],"TURKEY":["F SMY B"]},"results":{"F TRI":[""],"A BRE":[""],"A MAR":[""],"A WAR":[""],"F SMY":[""]},"messages":[{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1500,"phase":"W1902A","message":"Where are you from in real?"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1506,"phase":"W1902A","message":"why did you attack me?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1551,"phase":"W1902A","message":"Look get out of black sea plz"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":1553,"phase":"W1902A","message":"alright, well i am going to war with russia if u need help gime a holler"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1569,"phase":"W1902A","message":"Sorry about that i was supposed to move to tunis everything got messed up"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1596,"phase":"W1902A","message":"trust me i dont want to attack you i am going to help russia with austria and turkey"}]},{"name":"S1903M","state":{"timestamp":1537459329408336,"zobrist_hash":"3231737057642768600","note":"","name":"S1903M","units":{"AUSTRIA":["A BOH"],"ENGLAND":["A NWY","A LON","F NTH","F BAR"],"FRANCE":["A PAR","A SPA","F POR","A BRE","A MAR"],"GERMANY":["F HOL","A DEN","A BEL","A KIE","F HEL","A RUH"],"ITALY":["A VEN","F WES","F TYS","A TYR"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A STP","A VIE","A WAR"],"TURKEY":["F BLA","A SER","A BUL","F AEG","A ALB","F SMY"]},"centers":{"AUSTRIA":["TRI"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["TRI","BOH"],"ENGLAND":["LON","LVP","NWG","ENG","EDI","NWY","NTH","BAR"],"FRANCE":["BRE","MAR","PAR","MAO","SPA","POR"],"GERMANY":["BER","MUN","HOL","DEN","BEL","KIE","HEL","RUH"],"ITALY":["NAP","ROM","ION","TUN","PIE","VEN","WES","TYS","TYR"],"RUSSIA":["WAR","MOS","SEV","STP","UKR","FIN","BUD","RUM","SWE","GAL","VIE"],"TURKEY":["ANK","SMY","CON","BLA","GRE","SER","BUL","AEG","ALB"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BOH - MUN"],"ENGLAND":["A NWY - FIN","A LON - NWY VIA","F NTH C A LON - NWY","F BAR - STP\/NC"],"FRANCE":["A PAR S A MAR - BUR","A SPA - MAR","F POR - MAO","A BRE - PIC","A MAR - BUR"],"GERMANY":["F HOL - BEL","A DEN H","A BEL - BUR","F HEL - HOL","A KIE - MUN","A RUH S A BEL - BUR"],"ITALY":["F WES - TUN","F TYS - ION","A VEN - TRI","A TYR S A VEN - TRI"],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","F SWE S A STP - NWY","A BUD S A VIE","A MOS - STP","A STP - NWY","A VIE S A ALB - TRI","A WAR - LVN"],"TURKEY":["F BLA H","A SER S A ALB - TRI","A BUL S A SER","F AEG - ION","A ALB - TRI","F SMY - EAS"]},"results":{"A BOH":["bounce"],"A NWY":[],"A LON":["bounce"],"F NTH":[],"F BAR":["bounce"],"A PAR":[],"A SPA":["bounce"],"F POR":[],"A BRE":[],"A MAR":["bounce"],"F HOL":["bounce"],"A DEN":[],"A BEL":["bounce"],"A KIE":["bounce"],"F HEL":["bounce"],"A RUH":[],"A VEN":["bounce"],"F WES":[],"F TYS":["bounce"],"A TYR":[],"F SEV":[],"A BUD":[],"A RUM":[],"F SWE":[],"A MOS":["bounce"],"A STP":[],"A VIE":[],"A WAR":[],"F BLA":[],"A SER":[],"A BUL":[],"F AEG":["bounce"],"A ALB":[],"F SMY":[]},"messages":[{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1627,"phase":"S1903M","message":"Where are you from in real?"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1630,"phase":"S1903M","message":"same to you cause if you need help into sweden my piece in denmark is the ticket"},{"sender":"FRANCE","recipient":"ITALY","time_sent":1639,"phase":"S1903M","message":"ill believe you this time"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":1671,"phase":"S1903M","message":"How many tours what detachment?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":1724,"phase":"S1903M","message":"just one, i might get sent to afghanistan next,"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":1762,"phase":"S1903M","message":"sure next turn i am getting into position"},{"sender":"ITALY","recipient":"FRANCE","time_sent":1795,"phase":"S1903M","message":"okay yeah i will let you work on germany cause it looks like he is going to give you some trouble"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":1807,"phase":"S1903M","message":"okay sounds good"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":1882,"phase":"S1903M","message":"Stop repeating same thing I'm from the us why?"}]},{"name":"F1903M","state":{"timestamp":1537459329420831,"zobrist_hash":"8671786833365442395","note":"","name":"F1903M","units":{"AUSTRIA":["A BOH"],"ENGLAND":["A LON","F NTH","F BAR","A FIN"],"FRANCE":["A PAR","A SPA","A MAR","F MAO","A PIC"],"GERMANY":["F HOL","A DEN","A BEL","A KIE","F HEL","A RUH"],"ITALY":["A VEN","F TYS","A TYR","F TUN"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A VIE","A NWY","A LVN"],"TURKEY":["F BLA","A SER","A BUL","F AEG","A TRI","F EAS"]},"centers":{"AUSTRIA":["TRI"],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["LON","LVP","NWG","ENG","EDI","NTH","BAR","FIN"],"FRANCE":["BRE","MAR","PAR","SPA","POR","MAO","PIC"],"GERMANY":["BER","MUN","HOL","DEN","BEL","KIE","HEL","RUH"],"ITALY":["NAP","ROM","ION","PIE","VEN","WES","TYS","TYR","TUN"],"RUSSIA":["WAR","MOS","SEV","STP","UKR","BUD","RUM","SWE","GAL","VIE","NWY","LVN"],"TURKEY":["ANK","SMY","CON","BLA","GRE","SER","BUL","AEG","ALB","TRI","EAS"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BOH - MUN"],"ENGLAND":["A LON H","F NTH S A FIN - NWY","F BAR S A FIN - NWY","A FIN - NWY"],"FRANCE":["A PAR S A MAR - BUR","A SPA - MAR","A MAR - BUR","F MAO - ENG","A PIC S A MAR - BUR"],"GERMANY":["F HOL - BEL","A DEN H","A BEL - BUR","F HEL - HOL","A KIE - MUN","A RUH S A BEL - BUR"],"ITALY":["F TYS S F TUN - ION","A VEN H","A TYR S A VEN","F TUN - ION"],"RUSSIA":["F SEV H","A RUM H","F SWE S A NWY","A BUD H","A MOS S A LVN - STP","A VIE H","A LVN - STP","A NWY S F SWE"],"TURKEY":["F BLA H","A SER S A TRI","A BUL S A SER","F AEG S F EAS - ION","A TRI H","F EAS - ION"]},"results":{"A BOH":["bounce"],"A LON":[],"F NTH":[],"F BAR":[],"A FIN":[],"A PAR":[],"A SPA":[],"A MAR":[],"F MAO":[],"A PIC":[],"F HOL":["bounce"],"A DEN":[],"A BEL":["bounce"],"A KIE":["bounce"],"F HEL":["bounce"],"A RUH":[],"A VEN":[],"F TYS":[],"A TYR":[],"F TUN":["bounce"],"F SEV":[],"A BUD":[],"A RUM":[],"F SWE":[],"A MOS":[],"A VIE":[],"A NWY":["cut","dislodged"],"A LVN":[],"F BLA":[],"A SER":[],"A BUL":[],"F AEG":[],"A TRI":[],"F EAS":["bounce"]},"messages":[{"sender":"TURKEY","recipient":"RUSSIA","time_sent":1982,"phase":"F1903M","message":"Dont worry! Csak biztonságból tartom ott! Nem egyek sehova vele! Jól járunk mind a ketten a szövetségünkel!"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":2002,"phase":"F1903M","message":"I can only certainty there! Do not eat it anywhere! Well we were both in the alliance!"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":2014,"phase":"F1903M","message":"i literaly got shot at almost every time i went out in the field"}]},{"name":"W1903A","state":{"timestamp":1537459329423331,"zobrist_hash":"565726181450808049","note":"","name":"W1903A","units":{"AUSTRIA":["A BOH"],"ENGLAND":["A LON","F NTH","F BAR","A NWY"],"FRANCE":["A PAR","A PIC","A MAR","A BUR","F ENG"],"GERMANY":["F HOL","A DEN","A BEL","A KIE","F HEL","A RUH"],"ITALY":["A VEN","F TYS","A TYR","F TUN"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A VIE","A STP"],"TURKEY":["F BLA","A SER","A BUL","F AEG","A TRI","F EAS"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["LON","LVP","NWG","EDI","NTH","BAR","FIN","NWY"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","BUR","ENG"],"GERMANY":["BER","MUN","HOL","DEN","BEL","KIE","HEL","RUH"],"ITALY":["NAP","ROM","ION","PIE","VEN","WES","TYS","TYR","TUN"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","SWE","GAL","VIE","LVN","STP"],"TURKEY":["ANK","SMY","CON","BLA","GRE","SER","BUL","AEG","ALB","TRI","EAS"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":-1,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":1,"homes":["WAR"]},"TURKEY":{"count":1,"homes":["ANK","CON","SMY"]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":["A BOH D"],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["A WAR B"],"TURKEY":["F CON B"]},"results":{"A BOH":[""],"A WAR":[""],"F CON":[""],"A NWY":["disband"]},"messages":[{"sender":"RUSSIA","recipient":"TURKEY","time_sent":2346,"phase":"W1903A","message":"What"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2362,"phase":"W1903A","message":"do you plan on turning on turkey anytime soon cause i could help you there"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":2363,"phase":"W1903A","message":"u better or its the alternative"}]},{"name":"S1904M","state":{"timestamp":1537459329434917,"zobrist_hash":"1920079247224546725","note":"","name":"S1904M","units":{"AUSTRIA":[],"ENGLAND":["A LON","F NTH","F BAR","A NWY"],"FRANCE":["A PAR","A PIC","A MAR","A BUR","F ENG"],"GERMANY":["F HOL","A DEN","A BEL","A KIE","F HEL","A RUH"],"ITALY":["A VEN","F TYS","A TYR","F TUN"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A VIE","A STP","A WAR"],"TURKEY":["F BLA","A SER","A BUL","F AEG","A TRI","F EAS","F CON"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["LON","LVP","NWG","EDI","NTH","BAR","FIN","NWY"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","BUR","ENG"],"GERMANY":["BER","MUN","HOL","DEN","BEL","KIE","HEL","RUH"],"ITALY":["NAP","ROM","ION","PIE","VEN","WES","TYS","TYR","TUN"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","SWE","GAL","VIE","LVN","STP"],"TURKEY":["ANK","SMY","CON","BLA","GRE","SER","BUL","AEG","ALB","TRI","EAS"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["A LON - NWY VIA","F NTH C A LON - NWY","F BAR - STP\/NC","A NWY - SWE"],"FRANCE":["A PAR S A BUR","A PIC S F ENG - BEL","A MAR S A BUR","A BUR S F ENG - BEL","F ENG - BEL"],"GERMANY":["F HOL S A BEL","A DEN - KIE","A BEL H","F HEL - DEN","A KIE - MUN","A RUH S A BEL"],"ITALY":["F TYS S F TUN - ION","A VEN S A TYR","A TYR S A VEN","F TUN - ION"],"RUSSIA":["F SEV H","A RUM S A BUD","F SWE S A STP - NWY","A BUD S A VIE","A MOS - STP","A VIE - TYR","A STP - NWY","A WAR - LVN"],"TURKEY":["F BLA H","A SER S A TRI","A BUL S A SER","F AEG - GRE","A TRI H","F EAS - ION","F CON - AEG"]},"results":{"A LON":["bounce"],"F NTH":[],"F BAR":["bounce"],"A NWY":["bounce","dislodged"],"A PAR":[],"A PIC":[],"A MAR":[],"A BUR":[],"F ENG":["bounce"],"F HOL":[],"A DEN":[],"A BEL":[],"A KIE":[],"F HEL":[],"A RUH":[],"A VEN":[],"F TYS":[],"A TYR":["cut"],"F TUN":[],"F SEV":[],"A BUD":["void"],"A RUM":[],"F SWE":[],"A MOS":["bounce"],"A VIE":["bounce"],"A STP":[],"A WAR":[],"F BLA":[],"A SER":[],"A BUL":[],"F AEG":[],"A TRI":[],"F EAS":["bounce"],"F CON":[]},"messages":[{"sender":"TURKEY","recipient":"RUSSIA","time_sent":2434,"phase":"S1904M","message":"Well this will be the neutral zone when someone enters the war is ok?"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":2506,"phase":"S1904M","message":"can u support me to sweden from norway, and france told me he is going for belgium with fleet"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":2557,"phase":"S1904M","message":"can you support move me to belgium from english channel"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":2568,"phase":"S1904M","message":"Maybe"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":2612,"phase":"S1904M","message":"yeah i cant right now but in the fall i can"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":2614,"phase":"S1904M","message":"Just get out"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":2646,"phase":"S1904M","message":"okay well let me know when cause he is getting kind of big"}]},{"name":"S1904R","state":{"timestamp":1537459329436971,"zobrist_hash":"5843153123479130136","note":"","name":"S1904R","units":{"AUSTRIA":[],"ENGLAND":["A LON","F NTH","F BAR","*A NWY"],"FRANCE":["A PAR","A PIC","A MAR","A BUR","F ENG"],"GERMANY":["F HOL","A BEL","A RUH","A KIE","A MUN","F DEN"],"ITALY":["A VEN","F TYS","A TYR","F ION"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A VIE","A NWY","A LVN"],"TURKEY":["F BLA","A SER","A BUL","A TRI","F EAS","F GRE","F AEG"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["LON","LVP","NWG","EDI","NTH","BAR","FIN"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","BUR","ENG"],"GERMANY":["BER","HOL","BEL","HEL","RUH","KIE","MUN","DEN"],"ITALY":["NAP","ROM","PIE","VEN","WES","TYS","TYR","TUN","ION"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","SWE","GAL","VIE","STP","NWY","LVN"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","ALB","TRI","EAS","GRE","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["A NWY R FIN"],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A NWY":[]},"messages":[{"sender":"ENGLAND","recipient":"FRANCE","time_sent":2672,"phase":"S1904R","message":"next turn i am trying to get sweden"}]},{"name":"F1904M","state":{"timestamp":1537459329449319,"zobrist_hash":"1063405655659061469","note":"","name":"F1904M","units":{"AUSTRIA":[],"ENGLAND":["A LON","F NTH","F BAR","A FIN"],"FRANCE":["A PAR","A PIC","A MAR","A BUR","F ENG"],"GERMANY":["F HOL","A BEL","A RUH","A KIE","A MUN","F DEN"],"ITALY":["A VEN","F TYS","A TYR","F ION"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A MOS","A VIE","A NWY","A LVN"],"TURKEY":["F BLA","A SER","A BUL","A TRI","F EAS","F GRE","F AEG"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["LON","LVP","NWG","EDI","NTH","BAR","FIN"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","BUR","ENG"],"GERMANY":["BER","HOL","BEL","HEL","RUH","KIE","MUN","DEN"],"ITALY":["NAP","ROM","PIE","VEN","WES","TYS","TYR","TUN","ION"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","SWE","GAL","VIE","STP","NWY","LVN"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","ALB","TRI","EAS","GRE","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["A LON H","F NTH S A FIN - NWY","F BAR S A FIN - NWY","A FIN - NWY"],"FRANCE":["A PAR - BUR","A PIC S A BUR - BEL","A MAR S A PAR - BUR","A BUR - BEL","F ENG S A BUR - BEL"],"GERMANY":["F HOL S A BEL","A BEL H","A RUH - BUR","A KIE - RUH","A MUN S A RUH - BUR","F DEN - SWE"],"ITALY":["F TYS S F ION","A VEN S A TYR","A TYR S A VEN","F ION H"],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","F SWE S A NWY","A BUD S A VIE","A MOS - STP","A VIE S A BUD","A LVN S A MOS - STP","A NWY - FIN"],"TURKEY":["F BLA H","A SER S A TRI","A BUL S A SER","A TRI H","F EAS - ION","F AEG S F EAS - ION","F GRE S F EAS - ION"]},"results":{"A LON":[],"F NTH":[],"F BAR":[],"A FIN":[],"A PAR":["bounce"],"A PIC":[],"A MAR":[],"A BUR":[],"F ENG":[],"F HOL":[],"A BEL":["dislodged"],"A RUH":["bounce"],"A KIE":["bounce"],"A MUN":[],"F DEN":["bounce"],"A VEN":[],"F TYS":[],"A TYR":[],"F ION":["dislodged"],"F SEV":[],"A BUD":[],"A RUM":[],"F SWE":["void"],"A MOS":[],"A VIE":[],"A NWY":["bounce","dislodged"],"A LVN":[],"F BLA":[],"A SER":[],"A BUL":[],"A TRI":[],"F EAS":[],"F GRE":[],"F AEG":[]},"messages":[{"sender":"ENGLAND","recipient":"GERMANY","time_sent":2721,"phase":"F1904M","message":"ok"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":2744,"phase":"F1904M","message":"Ok"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":2791,"phase":"F1904M","message":"OK or not?"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":2842,"phase":"F1904M","message":"this stupid its a stalemate right now"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":2853,"phase":"F1904M","message":"oh ok ill try again anyway"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":2859,"phase":"F1904M","message":"okay where do you need me support now??"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":2911,"phase":"F1904M","message":"first i need to get back norway, then support from norway wen i have it back"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":2918,"phase":"F1904M","message":"Yep"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":2943,"phase":"F1904M","message":"go for it, just be careful dont let him get to powerful"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":2946,"phase":"F1904M","message":"okay"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":2960,"phase":"F1904M","message":"i can hit sweden for you to cut his support"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":2967,"phase":"F1904M","message":"Plz leave I'm multitasking can't explain"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":2993,"phase":"F1904M","message":"plz and france going for bel again he told me"}]},{"name":"F1904R","state":{"timestamp":1537459329451373,"zobrist_hash":"8990083941336526122","note":"","name":"F1904R","units":{"AUSTRIA":[],"ENGLAND":["A LON","F NTH","F BAR","A NWY"],"FRANCE":["A PAR","A PIC","A MAR","F ENG","A BEL"],"GERMANY":["F HOL","A RUH","A KIE","A MUN","F DEN"],"ITALY":["A VEN","F TYS","A TYR","*F ION"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A VIE","A LVN","A STP"],"TURKEY":["F BLA","A SER","A BUL","A TRI","F GRE","F AEG","F ION"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA"],"GERMANY":["BER","KIE","MUN","BEL","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["LON","LVP","NWG","EDI","NTH","BAR","FIN","NWY"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","BUR","ENG","BEL"],"GERMANY":["BER","HOL","HEL","RUH","KIE","MUN","DEN"],"ITALY":["NAP","ROM","PIE","VEN","WES","TYS","TYR","TUN"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","SWE","GAL","VIE","LVN","STP"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","ALB","TRI","EAS","GRE","AEG","ION"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":["F ION R ALB"],"RUSSIA":[],"TURKEY":[]},"results":{"F ION":[],"A NWY":["disband"],"A BEL":["disband"]},"messages":[{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3059,"phase":"F1904R","message":"ok now support me to sweden from norway"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":3106,"phase":"F1904R","message":"I'm going to the Italians! Yours in the northern territories!"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":3185,"phase":"F1904R","message":"okay is there anyway you could support me into belgium from ruhr"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3229,"phase":"F1904R","message":"first i need to convoy an army to norway then i will support"}]},{"name":"W1904A","state":{"timestamp":1537459329452758,"zobrist_hash":"3890453498983423669","note":"","name":"W1904A","units":{"AUSTRIA":[],"ENGLAND":["A LON","F NTH","F BAR","A NWY"],"FRANCE":["A PAR","A PIC","A MAR","F ENG","A BEL"],"GERMANY":["F HOL","A RUH","A KIE","A MUN","F DEN"],"ITALY":["A VEN","F TYS","A TYR","F ALB"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A VIE","A LVN","A STP"],"TURKEY":["F BLA","A SER","A BUL","A TRI","F GRE","F AEG","F ION"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA","BEL"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["LON","LVP","NWG","EDI","NTH","BAR","FIN","NWY"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","BUR","ENG","BEL"],"GERMANY":["BER","HOL","HEL","RUH","KIE","MUN","DEN"],"ITALY":["NAP","ROM","PIE","VEN","WES","TYS","TYR","TUN","ALB"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","SWE","GAL","VIE","LVN","STP"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","TRI","EAS","GRE","AEG","ION"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":1,"homes":["BRE"]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":1,"homes":["MOS","WAR"]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["F BRE B"],"GERMANY":[],"ITALY":[],"RUSSIA":["A MOS B"],"TURKEY":[]},"results":{"F BRE":[""],"A MOS":[""]},"messages":[{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":3246,"phase":"W1904A","message":"how long hav u been a soldier"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":3250,"phase":"W1904A","message":"okay sounds good"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":3298,"phase":"W1904A","message":"Few years I'm not yet though"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3303,"phase":"W1904A","message":"sorry for the delay, after i take some parts of russia then i will help u with invading france"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":3343,"phase":"W1904A","message":"So leave the black sea dammit"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":3345,"phase":"W1904A","message":"oh ok, been a Marine for 2 years"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":3349,"phase":"W1904A","message":"yeah no problem france is like all over me and i dont know what i can do about it anymore"}]},{"name":"S1905M","state":{"timestamp":1537459329465035,"zobrist_hash":"4799978510358319435","note":"","name":"S1905M","units":{"AUSTRIA":[],"ENGLAND":["A LON","F NTH","F BAR","A NWY"],"FRANCE":["A PAR","A PIC","A MAR","F ENG","A BEL","F BRE"],"GERMANY":["F HOL","A RUH","A KIE","A MUN","F DEN"],"ITALY":["A VEN","F TYS","A TYR","F ALB"],"RUSSIA":["F SEV","A BUD","A RUM","F SWE","A VIE","A LVN","A STP","A MOS"],"TURKEY":["F BLA","A SER","A BUL","A TRI","F GRE","F AEG","F ION"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA","BEL"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["LON","LVP","NWG","EDI","NTH","BAR","FIN","NWY"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","BUR","ENG","BEL"],"GERMANY":["BER","HOL","HEL","RUH","KIE","MUN","DEN"],"ITALY":["NAP","ROM","PIE","VEN","WES","TYS","TYR","TUN","ALB"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","SWE","GAL","VIE","LVN","STP"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","TRI","EAS","GRE","AEG","ION"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["A LON - NWY VIA","F NTH C A LON - NWY","F BAR - STP\/NC","A NWY - SWE"],"FRANCE":["A PAR - BUR","A PIC S A BEL","A MAR S A PAR - BUR","F ENG - IRI","A BEL S A PAR - BUR","F BRE - ENG"],"GERMANY":["F HOL - BEL","A RUH - BUR","A KIE - RUH","A MUN S A RUH - BUR","F DEN S A NWY - SWE"],"ITALY":["F TYS - NAP","A VEN S F ALB - TRI","A TYR S F ALB - TRI","F ALB - TRI"],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","F SWE S A STP - FIN","A BUD S A VIE","A VIE S A BUD","A LVN - STP","A STP - FIN","A MOS S A LVN - STP"],"TURKEY":["F BLA H","A SER S A TRI","A BUL H","A TRI H","F AEG H","F GRE H","F ION H"]},"results":{"A LON":[],"F NTH":[],"F BAR":["bounce"],"A NWY":[],"A PAR":["bounce"],"A PIC":[],"A MAR":[],"F ENG":[],"A BEL":["cut"],"F BRE":[],"F HOL":["bounce"],"A RUH":["bounce"],"A KIE":["bounce"],"A MUN":[],"F DEN":[],"A VEN":[],"F TYS":[],"A TYR":[],"F ALB":[],"F SEV":[],"A BUD":[],"A RUM":[],"F SWE":["cut","dislodged"],"A VIE":[],"A LVN":[],"A STP":[],"A MOS":[],"F BLA":[],"A SER":[],"A BUL":[],"A TRI":["dislodged"],"F GRE":[],"F AEG":[],"F ION":[]},"messages":[{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3399,"phase":"S1905M","message":"dont worry i will do wat i can"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3471,"phase":"S1905M","message":"dont forget to support me to swden from norway"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":3500,"phase":"S1905M","message":"yeah i did"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":3533,"phase":"S1905M","message":"I got two cousins in the corp"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":3547,"phase":"S1905M","message":"I got two cousins in the corp"},{"sender":"ITALY","recipient":"RUSSIA","time_sent":3575,"phase":"S1905M","message":"we could diminish turkey very quickly if we can work together"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":3595,"phase":"S1905M","message":"nice, next time u see them tell them semper fi"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3613,"phase":"S1905M","message":"ok then i will help u"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":3645,"phase":"S1905M","message":"I know just hold on a bit"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":3658,"phase":"S1905M","message":"okay sounds good cause he is going to be in burgundy picardy belgium and i am going to need all the support i can get to get back into belgium"}]},{"name":"S1905R","state":{"timestamp":1537459329467217,"zobrist_hash":"2589847691648061838","note":"","name":"S1905R","units":{"AUSTRIA":[],"ENGLAND":["F NTH","F BAR","A NWY","A SWE"],"FRANCE":["A PAR","A PIC","A MAR","A BEL","F IRI","F ENG"],"GERMANY":["F HOL","A RUH","A KIE","A MUN","F DEN"],"ITALY":["A VEN","A TYR","F NAP","F TRI"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A MOS","A STP","A FIN","*F SWE"],"TURKEY":["F BLA","A SER","A BUL","F GRE","F AEG","F ION"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA","BEL"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["LON","LVP","NWG","EDI","NTH","BAR","NWY","SWE"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","BUR","BEL","IRI","ENG"],"GERMANY":["BER","HOL","HEL","RUH","KIE","MUN","DEN"],"ITALY":["ROM","PIE","VEN","WES","TYS","TYR","TUN","ALB","NAP","TRI"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","GAL","VIE","LVN","STP","FIN"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","EAS","GRE","AEG","ION"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["F SWE R SKA"],"TURKEY":[]},"results":{"F SWE":[],"A TRI":["disband"]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":3679,"phase":"S1905R","message":"ok"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":3689,"phase":"S1905R","message":"Lol they don't talk to me anymore"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3711,"phase":"S1905R","message":"alright"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":3755,"phase":"S1905R","message":"oh sorry, yeah i hated iraq, i saw a bunch of crap i hav to live with forever"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":3756,"phase":"S1905R","message":"damn france just made a huge move on you, holy shit"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3793,"phase":"S1905R","message":"i know i need to get him the fuck away from me"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":3823,"phase":"S1905R","message":"yeah definitely"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":3834,"phase":"S1905R","message":"hey i was goin to support u to holland but no ur moving on me back off now"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3860,"phase":"S1905R","message":"i just txt him, he is being an ass"},{"sender":"GERMANY","recipient":"FRANCE","time_sent":3864,"phase":"S1905R","message":"so are you ready to work together yet, cause the way i see it the other half of the world is getting bigger and we arent moving anywhere"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":3874,"phase":"S1905R","message":"Support budapest to trieste so I can get in the fight"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":3881,"phase":"S1905R","message":"wait he is a friend of yours??"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3915,"phase":"S1905R","message":"no i meant txt him in the game"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":3944,"phase":"S1905R","message":"ohh right lol"},{"sender":"RUSSIA","recipient":"ENGLAND","time_sent":3956,"phase":"S1905R","message":"Lol only cause I decided to be a doggie, yeah I can only imagine"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":3957,"phase":"S1905R","message":"i almost got killed a couple of times too"}]},{"name":"F1905M","state":{"timestamp":1537459329479297,"zobrist_hash":"5612770968917288455","note":"","name":"F1905M","units":{"AUSTRIA":[],"ENGLAND":["F NTH","F BAR","A NWY","A SWE"],"FRANCE":["A PAR","A PIC","A MAR","A BEL","F IRI","F ENG"],"GERMANY":["F HOL","A RUH","A KIE","A MUN","F DEN"],"ITALY":["A VEN","A TYR","F NAP","F TRI"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A MOS","A STP","A FIN","F SKA"],"TURKEY":["F BLA","A SER","A BUL","F GRE","F AEG","F ION"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","LON","LVP","NWY"],"FRANCE":["BRE","MAR","PAR","POR","SPA","BEL"],"GERMANY":["BER","KIE","MUN","DEN","HOL"],"ITALY":["NAP","ROM","VEN","TUN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","SWE","VIE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["LON","LVP","NWG","EDI","NTH","BAR","NWY","SWE"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","BUR","BEL","IRI","ENG"],"GERMANY":["BER","HOL","HEL","RUH","KIE","MUN","DEN"],"ITALY":["ROM","PIE","VEN","WES","TYS","TYR","TUN","ALB","NAP","TRI"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","GAL","VIE","LVN","STP","FIN","SKA"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","EAS","GRE","AEG","ION"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NTH S A RUH - BEL","F BAR - NWG","A NWY S A SWE","A SWE S A NWY"],"FRANCE":["A PAR - BUR","A PIC S A BEL","A MAR S A PAR - BUR","A BEL S A PAR - BUR","F ENG - LON","F IRI - LVP"],"GERMANY":["F HOL S A RUH - BEL","A RUH - BEL","A KIE - RUH","A MUN S A KIE - RUH","F DEN S A SWE"],"ITALY":["A VEN - TRI","A TYR S A VEN - TRI","F TRI - ALB","F NAP - ION"],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","A BUD - TRI","A VIE S A BUD - TRI","A MOS - STP","A STP - NWY","A FIN S A STP - NWY","F SKA - SWE"],"TURKEY":["F BLA H","A SER - TRI","A BUL H","F AEG - GRE","F GRE - ION","F ION - TUN"]},"results":{"F NTH":[],"F BAR":[],"A NWY":["cut","dislodged"],"A SWE":["cut"],"A PAR":[],"A PIC":[],"A MAR":[],"A BEL":["cut","dislodged"],"F IRI":[],"F ENG":[],"F HOL":[],"A RUH":[],"A KIE":[],"A MUN":[],"F DEN":[],"A VEN":["bounce"],"A TYR":[],"F NAP":["bounce"],"F TRI":[],"F SEV":[],"A BUD":["bounce"],"A RUM":["void"],"A VIE":[],"A MOS":[],"A STP":[],"A FIN":[],"F SKA":["bounce"],"F BLA":[],"A SER":["bounce"],"A BUL":[],"F GRE":["bounce"],"F AEG":["bounce"],"F ION":[]},"messages":[{"sender":"ENGLAND","recipient":"GERMANY","time_sent":3983,"phase":"F1905M","message":"lol sorry i should have been specific"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":4084,"phase":"F1905M","message":"Oh no suth is mine! PLS support serbia to triest from budapest Thanks!"},{"sender":"FRANCE","recipient":"ENGLAND","time_sent":4121,"phase":"F1905M","message":"i wont attack you if you support me to holland also i was planning to help you against Russia with my one spare fleet."},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":4162,"phase":"F1905M","message":"no its okay so yeah what is your plan of action i am going to support your hold in sweden so you dont have to worry about that, and if you could support me into belgium from ruhr"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":4165,"phase":"F1905M","message":"ok"},{"sender":"ENGLAND","recipient":"GERMANY","time_sent":4191,"phase":"F1905M","message":"yeah ok"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":4238,"phase":"F1905M","message":"No I need it I'm going to lose sweden"},{"sender":"GERMANY","recipient":"ENGLAND","time_sent":4260,"phase":"F1905M","message":"i mean he is going to have to make the decision on if he is going to lose belgium which would make me gain range on him on land"},{"sender":"ENGLAND","recipient":"RUSSIA","time_sent":4262,"phase":"F1905M","message":"wat do u do in the army"}]},{"name":"W1905A","state":{"timestamp":1537459329482487,"zobrist_hash":"2427521039261932610","note":"","name":"W1905A","units":{"AUSTRIA":[],"ENGLAND":["F NTH","A SWE","F NWG"],"FRANCE":["A PIC","A MAR","A BUR","F LVP","F LON"],"GERMANY":["F HOL","A MUN","F DEN","A BEL","A RUH"],"ITALY":["A VEN","A TYR","F NAP","F ALB"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A FIN","F SKA","A STP","A NWY"],"TURKEY":["F BLA","A SER","A BUL","F GRE","F AEG","F TUN"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","SWE"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["EDI","NTH","BAR","SWE","NWG"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","IRI","ENG","BUR","LVP","LON"],"GERMANY":["BER","HOL","HEL","KIE","MUN","DEN","BEL","RUH"],"ITALY":["ROM","PIE","VEN","WES","TYS","TYR","NAP","TRI","ALB"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","EAS","GRE","AEG","ION","TUN"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":-1,"homes":[]},"FRANCE":{"count":2,"homes":["BRE","PAR"]},"GERMANY":{"count":1,"homes":["BER","KIE"]},"ITALY":{"count":-1,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":2,"homes":["ANK","CON","SMY"]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["A SWE D"],"FRANCE":["F BRE B","A PAR B"],"GERMANY":["A KIE B"],"ITALY":["F ALB D"],"RUSSIA":[],"TURKEY":["A CON B","A SMY B"]},"results":{"A SWE":[""],"F BRE":[""],"A PAR":[""],"A KIE":[""],"F ALB":[""],"A CON":[""],"A SMY":[""],"A BEL":["disband"],"A NWY":["disband"]},"messages":[{"sender":"ENGLAND","recipient":"GERMANY","time_sent":4336,"phase":"W1905A","message":"fucking a, its all good, this sucks"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":4363,"phase":"W1905A","message":"Oh no suth is mine! PLS support serbia to triest from budapest Thanks!"},{"sender":"GERMANY","recipient":"FRANCE","time_sent":4394,"phase":"W1905A","message":"okay cause this is getting ridiculous"},{"sender":"ENGLAND","recipient":"FRANCE","time_sent":4428,"phase":"W1905A","message":"wow watta lier"}]},{"name":"S1906M","state":{"timestamp":1537459329493903,"zobrist_hash":"7681366955669937015","note":"","name":"S1906M","units":{"AUSTRIA":[],"ENGLAND":["F NTH","F NWG"],"FRANCE":["A PIC","A MAR","A BUR","F LVP","F LON","F BRE","A PAR"],"GERMANY":["F HOL","A MUN","F DEN","A BEL","A RUH","A KIE"],"ITALY":["A VEN","A TYR","F NAP"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A FIN","F SKA","A STP","A NWY"],"TURKEY":["F BLA","A SER","A BUL","F GRE","F AEG","F TUN","A CON","A SMY"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","SWE"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["EDI","NTH","BAR","SWE","NWG"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","IRI","ENG","BUR","LVP","LON"],"GERMANY":["BER","HOL","HEL","KIE","MUN","DEN","BEL","RUH"],"ITALY":["ROM","PIE","VEN","WES","TYS","TYR","NAP","TRI","ALB"],"RUSSIA":["WAR","MOS","SEV","UKR","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","EAS","GRE","AEG","ION","TUN"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NTH - EDI","F NWG S F NTH - EDI"],"FRANCE":["A PIC S A BUR","A MAR S A BUR","A BUR S A PIC","F LVP - CLY","F LON S F BRE - ENG","F BRE - ENG","A PAR S A BUR"],"GERMANY":["F HOL - NTH","A MUN - SIL","F DEN S F HOL - NTH","A RUH S A BEL","A BEL H","A KIE - MUN"],"ITALY":["A VEN - TRI","A TYR S A VEN - TRI","F NAP - TYS"],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","A BUD - TRI","A VIE S A BUD - TRI","A FIN - SWE","F SKA S A NWY","A STP - MOS","A NWY S A FIN - SWE"],"TURKEY":["F BLA H","A SER - TRI","A BUL - SER","F AEG - ION","F GRE - ALB","F TUN - TYS","A CON - BUL VIA","A SMY - CON VIA"]},"results":{"F NTH":[],"F NWG":[],"A PIC":[],"A MAR":[],"A BUR":[],"F LVP":[],"F LON":[],"F BRE":[],"A PAR":[],"F HOL":[],"A MUN":[],"F DEN":[],"A BEL":[],"A RUH":[],"A KIE":[],"A VEN":["bounce"],"A TYR":[],"F NAP":["bounce"],"F SEV":[],"A BUD":["bounce"],"A RUM":["void"],"A VIE":[],"A FIN":[],"F SKA":[],"A STP":[],"A NWY":[],"F BLA":[],"A SER":["bounce"],"A BUL":["bounce"],"F GRE":[],"F AEG":[],"F TUN":["bounce"],"A CON":["bounce"],"A SMY":["bounce"]},"messages":[{"sender":"RUSSIA","recipient":"TURKEY","time_sent":4441,"phase":"S1906M","message":"Your being a shitty ally dude"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":4500,"phase":"S1906M","message":"Thanks my friend! ;)"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":4640,"phase":"S1906M","message":"Are you serious"}]},{"name":"F1906M","state":{"timestamp":1537459329507022,"zobrist_hash":"7579890997189983831","note":"","name":"F1906M","units":{"AUSTRIA":[],"ENGLAND":["F NWG","F EDI"],"FRANCE":["A PIC","A MAR","A BUR","F LON","A PAR","F CLY","F ENG"],"GERMANY":["F DEN","A BEL","A RUH","F NTH","A SIL","A MUN"],"ITALY":["A VEN","A TYR","F NAP"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","F SKA","A NWY","A SWE","A MOS"],"TURKEY":["F BLA","A SER","A BUL","F TUN","A CON","A SMY","F ALB","F ION"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI","SWE"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["BAR","NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","IRI","BUR","LVP","LON","CLY","ENG"],"GERMANY":["BER","HOL","HEL","KIE","DEN","BEL","RUH","NTH","SIL","MUN"],"ITALY":["ROM","PIE","VEN","WES","TYS","TYR","NAP","TRI"],"RUSSIA":["WAR","SEV","UKR","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","EAS","GRE","AEG","TUN","ALB","ION"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NWG - CLY","F EDI H"],"FRANCE":["A PIC - BEL","A MAR S A PAR - BUR","A BUR - RUH","F LON S F ENG","A PAR - BUR","F CLY - EDI","F ENG C A PIC - BEL"],"GERMANY":["F DEN S F NTH","A RUH S A BEL","A BEL H","A MUN - BUR","A SIL - WAR","F NTH S A BEL"],"ITALY":["A VEN - TRI","A TYR S A VEN - TRI","F NAP - TYS"],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","A BUD S A VIE","A VIE S A BUD","F SKA - DEN","A NWY S A SWE","A MOS - WAR","A SWE S A NWY"],"TURKEY":["F BLA H","A SER - TRI","A BUL - GRE","F TUN H","A CON H","A SMY - CON","F ALB S A SER - TRI","F ION - NAP"]},"results":{"F NWG":["bounce"],"F EDI":[],"A PIC":["bounce"],"A MAR":[],"A BUR":["bounce"],"F LON":[],"A PAR":["bounce"],"F CLY":["bounce"],"F ENG":[],"F DEN":["cut"],"A BEL":[],"A RUH":["cut"],"F NTH":[],"A SIL":["bounce"],"A MUN":["bounce"],"A VEN":["bounce"],"A TYR":[],"F NAP":[],"F SEV":[],"A BUD":[],"A RUM":[],"A VIE":[],"F SKA":["bounce"],"A NWY":[],"A SWE":[],"A MOS":["bounce"],"F BLA":[],"A SER":["bounce"],"A BUL":[],"F TUN":[],"A CON":[],"A SMY":["bounce"],"F ALB":[],"F ION":[]},"messages":[{"sender":"ITALY","recipient":"RUSSIA","time_sent":4856,"phase":"F1906M","message":"could you finally support me into trieste instead of bouncing me out of it"},{"sender":"ENGLAND","recipient":"GLOBAL","time_sent":4866,"phase":"F1906M","message":"well i dont make a good general in the military but i am a good Marine on the ground though lol"},{"sender":"RUSSIA","recipient":"ITALY","time_sent":4934,"phase":"F1906M","message":"How about suppport me there"}]},{"name":"W1906A","state":{"timestamp":1537459329509655,"zobrist_hash":"5105082817748223020","note":"","name":"W1906A","units":{"AUSTRIA":[],"ENGLAND":["F NWG","F EDI"],"FRANCE":["A PIC","A MAR","A BUR","F LON","A PAR","F CLY","F ENG"],"GERMANY":["F DEN","A BEL","A RUH","F NTH","A SIL","A MUN"],"ITALY":["A VEN","A TYR","F TYS"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","F SKA","A NWY","A SWE","A MOS"],"TURKEY":["F BLA","A SER","F TUN","A CON","A SMY","F ALB","A GRE","F NAP"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["BAR","NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","IRI","BUR","LVP","LON","CLY","ENG"],"GERMANY":["BER","HOL","HEL","KIE","DEN","BEL","RUH","NTH","SIL","MUN"],"ITALY":["ROM","PIE","VEN","WES","TYR","TRI","TYS"],"RUSSIA":["WAR","SEV","UKR","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","EAS","AEG","TUN","ALB","ION","GRE","NAP"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":-1,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":-1,"homes":[]},"RUSSIA":{"count":1,"homes":["STP","WAR"]},"TURKEY":{"count":1,"homes":["ANK"]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F NWG D"],"FRANCE":[],"GERMANY":[],"ITALY":["F TYS D"],"RUSSIA":["A WAR B"],"TURKEY":["A ANK B"]},"results":{"F NWG":[""],"F TYS":[""],"A WAR":[""],"A ANK":[""]},"messages":[{"sender":"TURKEY","recipient":"RUSSIA","time_sent":5082,"phase":"W1906A","message":"Yeah i can heave triest becuse you and italien can no go to Triest!"},{"sender":"ITALY","recipient":"GLOBAL","time_sent":5097,"phase":"W1906A","message":"im out peace"}]},{"name":"S1907M","state":{"timestamp":1537459329520519,"zobrist_hash":"2519428787844225472","note":"","name":"S1907M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A PIC","A MAR","A BUR","F LON","A PAR","F CLY","F ENG"],"GERMANY":["F DEN","A BEL","A RUH","F NTH","A SIL","A MUN"],"ITALY":["A VEN","A TYR"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","F SKA","A NWY","A SWE","A MOS","A WAR"],"TURKEY":["F BLA","A SER","F TUN","A CON","A SMY","F ALB","A GRE","F NAP","A ANK"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["BAR","NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAO","PIC","MAR","IRI","BUR","LVP","LON","CLY","ENG"],"GERMANY":["BER","HOL","HEL","KIE","DEN","BEL","RUH","NTH","SIL","MUN"],"ITALY":["ROM","PIE","VEN","WES","TYR","TRI","TYS"],"RUSSIA":["WAR","SEV","UKR","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS"],"TURKEY":["ANK","SMY","CON","BLA","SER","BUL","EAS","AEG","TUN","ALB","ION","GRE","NAP"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A PIC - BEL","A MAR H","A BUR - RUH","F LON - NTH","A PAR - PIC","F CLY - EDI","F ENG S A PIC - BEL"],"GERMANY":["F DEN H","A RUH H","A BEL H","A MUN H","A SIL H","F NTH H"],"ITALY":["A VEN H","A TYR H"],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","A BUD - TRI","A VIE S A BUD - TRI","F SKA - DEN","A NWY S A SWE","A MOS - WAR","A SWE S F SKA - DEN","A WAR - PRU"],"TURKEY":["F BLA H","A SER - TRI","F TUN - TYS","A CON - BUL VIA","A SMY - CON","F ALB S A SER - TRI","A GRE H","F NAP - ROM","A ANK H"]},"results":{"F EDI":[],"A PIC":[],"A MAR":[],"A BUR":["bounce"],"F LON":["bounce"],"A PAR":[],"F CLY":["bounce"],"F ENG":[],"F DEN":["dislodged"],"A BEL":["dislodged"],"A RUH":[],"F NTH":[],"A SIL":[],"A MUN":[],"A VEN":[],"A TYR":[],"F SEV":[],"A BUD":["bounce"],"A RUM":["void"],"A VIE":[],"F SKA":[],"A NWY":[],"A SWE":[],"A MOS":[],"A WAR":[],"F BLA":[],"A SER":["bounce"],"F TUN":[],"A CON":[],"A SMY":[],"F ALB":[],"A GRE":[],"F NAP":[],"A ANK":[]},"messages":[{"sender":"TURKEY","recipient":"RUSSIA","time_sent":5169,"phase":"S1907M","message":"Yeah i can heave triest becuse you and italien can no go to Triest!"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":5231,"phase":"S1907M","message":"Look your being a douche you won't leave fucking black sea I'm doing all the fighting and I need more fucking armies"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":5236,"phase":"S1907M","message":"Caqn yyou suppoert me Sebia-->Triest?"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":5421,"phase":"S1907M","message":"Do you even fucking listen"}]},{"name":"S1907R","state":{"timestamp":1537459329522498,"zobrist_hash":"2212831072421129284","note":"","name":"S1907R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A MAR","A BUR","F LON","F CLY","F ENG","A BEL","A PIC"],"GERMANY":["A RUH","F NTH","A SIL","A MUN","*F DEN","*A BEL"],"ITALY":["A VEN","A TYR"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A NWY","A SWE","F DEN","A WAR","A PRU"],"TURKEY":["F BLA","A SER","F ALB","A GRE","A ANK","F TYS","A BUL","A CON","F ROM"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["BAR","NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAO","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC"],"GERMANY":["BER","HOL","HEL","KIE","RUH","NTH","SIL","MUN"],"ITALY":["PIE","VEN","WES","TYR","TRI"],"RUSSIA":["SEV","UKR","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS","DEN","WAR","PRU"],"TURKEY":["ANK","SMY","BLA","SER","EAS","AEG","TUN","ALB","ION","GRE","NAP","TYS","BUL","CON","ROM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":["A BEL D","F DEN D"],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"F DEN":["disband"],"A BEL":["disband"]},"messages":[{"sender":"RUSSIA","recipient":"TURKEY","time_sent":5598,"phase":"S1907R","message":"Yeah fine you can have it sorry"}]},{"name":"F1907M","state":{"timestamp":1537459329532286,"zobrist_hash":"7323618956941488557","note":"","name":"F1907M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A MAR","A BUR","F LON","F CLY","F ENG","A BEL","A PIC"],"GERMANY":["A RUH","F NTH","A SIL","A MUN"],"ITALY":["A VEN","A TYR"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A NWY","A SWE","F DEN","A WAR","A PRU"],"TURKEY":["F BLA","A SER","F ALB","A GRE","A ANK","F TYS","A BUL","A CON","F ROM"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP"],"GERMANY":["BER","KIE","MUN","DEN","HOL","BEL"],"ITALY":["ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["BAR","NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAO","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC"],"GERMANY":["BER","HOL","HEL","KIE","RUH","NTH","SIL","MUN"],"ITALY":["PIE","VEN","WES","TYR","TRI"],"RUSSIA":["SEV","UKR","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS","DEN","WAR","PRU"],"TURKEY":["ANK","SMY","BLA","SER","EAS","AEG","TUN","ALB","ION","GRE","NAP","TYS","BUL","CON","ROM"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A MAR H","A BUR H","F LON H","F CLY H","F ENG H","A BEL H","A PIC H"],"GERMANY":["A RUH H","A MUN H","A SIL H","F NTH H"],"ITALY":["A VEN H","A TYR H"],"RUSSIA":["F SEV S A RUM","A RUM H","A BUD S A VIE","A VIE S A BUD","A NWY S A SWE","A SWE - DEN","A PRU - BER","A WAR - SIL","F DEN - KIE"],"TURKEY":["F BLA H","A SER - TRI","F ALB S A SER - TRI","A GRE H","A ANK - CON VIA","F TYS S F ROM","F ROM H","A BUL - SER","A CON - BUL VIA"]},"results":{"F EDI":[],"A MAR":[],"A BUR":[],"F LON":[],"F CLY":[],"F ENG":[],"A BEL":[],"A PIC":[],"A RUH":[],"F NTH":[],"A SIL":[],"A MUN":[],"A VEN":[],"A TYR":[],"F SEV":[],"A BUD":[],"A RUM":[],"A VIE":[],"A NWY":["void"],"A SWE":[],"F DEN":[],"A WAR":["bounce"],"A PRU":[],"F BLA":[],"A SER":[],"F ALB":[],"A GRE":[],"A ANK":[],"F TYS":[],"A BUL":[],"A CON":[],"F ROM":[]},"messages":[]},{"name":"W1907A","state":{"timestamp":1537459329534981,"zobrist_hash":"7053253268352212670","note":"","name":"W1907A","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A MAR","A BUR","F LON","F CLY","F ENG","A BEL","A PIC"],"GERMANY":["A RUH","F NTH","A SIL","A MUN"],"ITALY":["A VEN","A TYR"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A NWY","A WAR","A DEN","F KIE","A BER"],"TURKEY":["F BLA","F ALB","A GRE","F TYS","F ROM","A TRI","A CON","A SER","A BUL"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL"],"GERMANY":["MUN","HOL"],"ITALY":["VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["BAR","NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAO","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC"],"GERMANY":["HOL","HEL","RUH","NTH","SIL","MUN"],"ITALY":["PIE","VEN","WES","TYR"],"RUSSIA":["SEV","UKR","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS","WAR","PRU","DEN","KIE","BER"],"TURKEY":["ANK","SMY","BLA","EAS","AEG","TUN","ALB","ION","GRE","NAP","TYS","ROM","TRI","CON","SER","BUL"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":1,"homes":["BRE","PAR"]},"GERMANY":{"count":-2,"homes":[]},"ITALY":{"count":-1,"homes":[]},"RUSSIA":{"count":2,"homes":["MOS","STP"]},"TURKEY":{"count":1,"homes":["ANK","SMY"]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["F BRE B"],"GERMANY":["A SIL D","F NTH D"],"ITALY":["A TYR D"],"RUSSIA":["A MOS B","F STP\/NC B"],"TURKEY":["F SMY B"]},"results":{"F BRE":[""],"A SIL":[""],"F NTH":[""],"A TYR":[""],"A MOS":[""],"F STP\/NC":[""],"F SMY":[""]},"messages":[{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":6209,"phase":"W1907A","message":"You wanna draw turkey no one is here anymore"}]},{"name":"S1908M","state":{"timestamp":1537459329544587,"zobrist_hash":"1386805156008447503","note":"","name":"S1908M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A MAR","A BUR","F LON","F CLY","F ENG","A BEL","A PIC","F BRE"],"GERMANY":["A RUH","A MUN"],"ITALY":["A VEN"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A NWY","A WAR","A DEN","F KIE","A BER","A MOS","F STP\/NC"],"TURKEY":["F BLA","F ALB","A GRE","F TYS","F ROM","A TRI","A CON","A SER","A BUL","F SMY"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL"],"GERMANY":["MUN","HOL"],"ITALY":["VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["BAR","NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAO","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC"],"GERMANY":["HOL","HEL","RUH","NTH","SIL","MUN"],"ITALY":["PIE","VEN","WES","TYR"],"RUSSIA":["SEV","UKR","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS","WAR","PRU","DEN","KIE","BER"],"TURKEY":["ANK","SMY","BLA","EAS","AEG","TUN","ALB","ION","GRE","NAP","TYS","ROM","TRI","CON","SER","BUL"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A MAR H","A BUR H","F LON H","F CLY H","F ENG H","A BEL H","A PIC H","F BRE H"],"GERMANY":["A RUH - HOL","A MUN H"],"ITALY":["A VEN H"],"RUSSIA":["F SEV S A RUM","A RUM H","A BUD S A RUM","A VIE - TYR","A NWY H","A WAR - SIL","A DEN - KIE","F KIE - HEL","A BER - MUN","A MOS - WAR","F STP\/NC - BAR"],"TURKEY":["F BLA H","F ALB - ADR","A GRE - SER","F TYS - TUN","F ROM H","A TRI - TYR","A SER - TRI","A BUL - SER","A CON - BUL VIA","F SMY - AEG"]},"results":{"F EDI":[],"A MAR":[],"A BUR":[],"F LON":[],"F CLY":[],"F ENG":[],"A BEL":[],"A PIC":[],"F BRE":[],"A RUH":[],"A MUN":[],"A VEN":[],"F SEV":[],"A BUD":[],"A RUM":[],"A VIE":["bounce"],"A NWY":[],"A WAR":[],"A DEN":[],"F KIE":[],"A BER":["bounce"],"A MOS":[],"F STP\/NC":[],"F BLA":[],"F ALB":[],"A GRE":["bounce"],"F TYS":[],"F ROM":[],"A TRI":["bounce"],"A CON":["bounce"],"A SER":["bounce"],"A BUL":["bounce"],"F SMY":[]},"messages":[]},{"name":"F1908M","state":{"timestamp":1537459329555310,"zobrist_hash":"3037795413169545545","note":"","name":"F1908M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A MAR","A BUR","F LON","F CLY","F ENG","A BEL","A PIC","F BRE"],"GERMANY":["A MUN","A HOL"],"ITALY":["A VEN"],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A NWY","A BER","A SIL","A KIE","F HEL","A WAR","F BAR"],"TURKEY":["F BLA","A GRE","F ROM","A TRI","A CON","A SER","A BUL","F ADR","F TUN","F AEG"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL"],"GERMANY":["MUN","HOL"],"ITALY":["VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAO","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC"],"GERMANY":["RUH","NTH","MUN","HOL"],"ITALY":["PIE","VEN","WES","TYR"],"RUSSIA":["SEV","UKR","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS","PRU","DEN","BER","SIL","KIE","HEL","WAR","BAR"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","ION","GRE","NAP","TYS","ROM","TRI","CON","SER","BUL","ADR","TUN","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A MAR - PIE","A BUR S A MUN","F LON - NTH","F CLY - NWG","F ENG S F LON - NTH","A BEL S A BUR - RUH","A PIC - BUR","F BRE - MAO"],"GERMANY":["A MUN H","A HOL H"],"ITALY":["A VEN H"],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","A BUD S A VIE","A VIE - TYR","A NWY H","A BER - MUN","A SIL S A BER - MUN","A WAR - UKR","A KIE - HOL","F HEL S A KIE - HOL","F BAR - NWG"],"TURKEY":["F BLA H","A GRE H","F ROM H","A TRI H","A SER - TRI","A BUL H","A CON H","F ADR S A TRI - VEN","F AEG H","F TUN H"]},"results":{"F EDI":[],"A MAR":[],"A BUR":[],"F LON":[],"F CLY":["bounce"],"F ENG":[],"A BEL":["void"],"A PIC":["bounce"],"F BRE":[],"A MUN":[],"A HOL":["dislodged"],"A VEN":[],"F SEV":[],"A BUD":["void"],"A RUM":[],"A VIE":[],"A NWY":[],"A BER":["bounce"],"A SIL":[],"A KIE":[],"F HEL":[],"A WAR":[],"F BAR":["bounce"],"F BLA":[],"A GRE":[],"F ROM":[],"A TRI":[],"A CON":[],"A SER":["bounce"],"A BUL":[],"F ADR":["void"],"F TUN":[],"F AEG":[]},"messages":[{"sender":"TURKEY","recipient":"RUSSIA","time_sent":6813,"phase":"F1908M","message":"Whera are you from in real? :D"},{"sender":"FRANCE","recipient":"GLOBAL","time_sent":6853,"phase":"F1908M","message":"how about a 3 way draw?"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":6960,"phase":"F1908M","message":"Please go through Bohemia to Munich, I need to go to Venice TRY"}]},{"name":"F1908R","state":{"timestamp":1537459329557264,"zobrist_hash":"2769996607605403746","note":"","name":"F1908R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F ENG","A BEL","A PIC","A PIE","F NTH","F MAO"],"GERMANY":["A MUN","*A HOL"],"ITALY":["A VEN"],"RUSSIA":["F SEV","A BUD","A RUM","A NWY","A BER","A SIL","F HEL","F BAR","A TYR","A HOL","A UKR"],"TURKEY":["F BLA","A GRE","F ROM","A TRI","A CON","A SER","A BUL","F ADR","F TUN","F AEG"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL"],"GERMANY":["MUN","HOL"],"ITALY":["VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC","PIE","NTH","MAO"],"GERMANY":["RUH","MUN"],"ITALY":["VEN","WES"],"RUSSIA":["SEV","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS","PRU","DEN","BER","SIL","KIE","HEL","WAR","BAR","TYR","HOL","UKR"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","ION","GRE","NAP","TYS","ROM","TRI","CON","SER","BUL","ADR","TUN","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":["A HOL D"],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A HOL":["disband"]},"messages":[{"sender":"TURKEY","recipient":"RUSSIA","time_sent":7033,"phase":"F1908R","message":"Please go through Bohemia to Munich, I need to go to Venice TRY"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":7090,"phase":"F1908R","message":"Please go through Bohemia to Munich, I need to go to Venice TRY"},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":7131,"phase":"F1908R","message":"Germany won't leave"},{"sender":"FRANCE","recipient":"GLOBAL","time_sent":7218,"phase":"F1908R","message":"he'll leave if he doesnt do anything this turn"},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":7272,"phase":"F1908R","message":"Fine as soon as he's. Gone"}]},{"name":"W1908A","state":{"timestamp":1537459329558720,"zobrist_hash":"4515407687089423606","note":"","name":"W1908A","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F ENG","A BEL","A PIC","A PIE","F NTH","F MAO"],"GERMANY":["A MUN"],"ITALY":["A VEN"],"RUSSIA":["F SEV","A BUD","A RUM","A NWY","A BER","A SIL","F HEL","F BAR","A TYR","A HOL","A UKR"],"TURKEY":["F BLA","A GRE","F ROM","A TRI","A CON","A SER","A BUL","F ADR","F TUN","F AEG"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL"],"GERMANY":["MUN"],"ITALY":["VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","HOL"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC","PIE","NTH","MAO"],"GERMANY":["RUH","MUN"],"ITALY":["VEN","WES"],"RUSSIA":["SEV","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS","PRU","DEN","BER","SIL","KIE","HEL","WAR","BAR","TYR","HOL","UKR"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","ION","GRE","NAP","TYS","ROM","TRI","CON","SER","BUL","ADR","TUN","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":2,"homes":["MOS","STP","WAR"]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["F STP\/NC B","A WAR B"],"TURKEY":[]},"results":{"F STP\/NC":[""],"A WAR":[""]},"messages":[{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":7423,"phase":"W1908A","message":"Let's draw this is boring"}]},{"name":"S1909M","state":{"timestamp":1537459329570329,"zobrist_hash":"561324046783023714","note":"","name":"S1909M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F ENG","A BEL","A PIC","A PIE","F NTH","F MAO"],"GERMANY":["A MUN"],"ITALY":["A VEN"],"RUSSIA":["F SEV","A BUD","A RUM","A NWY","A BER","A SIL","F HEL","F BAR","A TYR","A HOL","A UKR","F STP\/NC","A WAR"],"TURKEY":["F BLA","A GRE","F ROM","A TRI","A CON","A SER","A BUL","F ADR","F TUN","F AEG"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL"],"GERMANY":["MUN"],"ITALY":["VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","HOL"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["NWG","EDI"],"FRANCE":["BRE","PAR","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC","PIE","NTH","MAO"],"GERMANY":["RUH","MUN"],"ITALY":["VEN","WES"],"RUSSIA":["SEV","BUD","RUM","GAL","VIE","LVN","FIN","SKA","STP","NWY","SWE","MOS","PRU","DEN","BER","SIL","KIE","HEL","WAR","BAR","TYR","HOL","UKR"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","ION","GRE","NAP","TYS","ROM","TRI","CON","SER","BUL","ADR","TUN","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A BUR S A MUN","F CLY - NWG","F ENG - NTH","A BEL - HOL","A PIC - PAR","F NTH - DEN","F MAO - WES","A PIE - TYR"],"GERMANY":["A MUN H"],"ITALY":["A VEN H"],"RUSSIA":["F SEV H","A RUM H","A BUD H","A NWY H","A BER H","A SIL H","F HEL - DEN","F BAR - NWG","A UKR S A RUM","A HOL H","A TYR - VIE","A WAR H","F STP\/NC - BAR"],"TURKEY":["F BLA H","A GRE H","F ROM - TUS","A TRI - VEN","A SER - TRI","A BUL - SER","A CON - BUL","F ADR S A TRI - VEN","F AEG H","F TUN - WES"]},"results":{"F EDI":[],"A BUR":[],"F CLY":["bounce"],"F ENG":["bounce"],"A BEL":["bounce"],"A PIC":[],"A PIE":[],"F NTH":["bounce"],"F MAO":["bounce"],"A MUN":[],"A VEN":["dislodged"],"F SEV":[],"A BUD":[],"A RUM":[],"A NWY":[],"A BER":[],"A SIL":[],"F HEL":["bounce"],"F BAR":["bounce"],"A TYR":[],"A HOL":[],"A UKR":[],"F STP\/NC":["bounce"],"A WAR":[],"F BLA":[],"A GRE":[],"F ROM":[],"A TRI":[],"A CON":[],"A SER":[],"A BUL":[],"F ADR":[],"F TUN":["bounce"],"F AEG":[]},"messages":[{"sender":"RUSSIA","recipient":"TURKEY","time_sent":7709,"phase":"S1909M","message":"Draw!"},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":7741,"phase":"S1909M","message":"Draw please"}]},{"name":"S1909R","state":{"timestamp":1537459329572461,"zobrist_hash":"3521850666727299927","note":"","name":"S1909R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F ENG","A BEL","F NTH","F MAO","A PAR","A TYR"],"GERMANY":["A MUN"],"ITALY":["*A VEN"],"RUSSIA":["F SEV","A BUD","A RUM","A NWY","A BER","A SIL","F HEL","F BAR","A HOL","A UKR","F STP\/NC","A WAR","A VIE"],"TURKEY":["F BLA","A GRE","F ADR","F TUN","F AEG","F TUS","A VEN","A BUL","A TRI","A SER"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL"],"GERMANY":["MUN"],"ITALY":["VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","HOL"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["NWG","EDI"],"FRANCE":["BRE","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC","PIE","NTH","MAO","PAR","TYR"],"GERMANY":["RUH","MUN"],"ITALY":["WES"],"RUSSIA":["SEV","BUD","RUM","GAL","LVN","FIN","SKA","STP","NWY","SWE","MOS","PRU","DEN","BER","SIL","KIE","HEL","WAR","BAR","HOL","UKR","VIE"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","ION","GRE","NAP","TYS","ROM","CON","ADR","TUN","AEG","TUS","VEN","BUL","TRI","SER"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":["A VEN D"],"RUSSIA":[],"TURKEY":[]},"results":{"A VEN":["disband"]},"messages":[]},{"name":"F1909M","state":{"timestamp":1537459329583824,"zobrist_hash":"5872361043318800307","note":"","name":"F1909M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F ENG","A BEL","F NTH","F MAO","A PAR","A TYR"],"GERMANY":["A MUN"],"ITALY":[],"RUSSIA":["F SEV","A BUD","A RUM","A NWY","A BER","A SIL","F HEL","F BAR","A HOL","A UKR","F STP\/NC","A WAR","A VIE"],"TURKEY":["F BLA","A GRE","F ADR","F TUN","F AEG","F TUS","A VEN","A BUL","A TRI","A SER"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL"],"GERMANY":["MUN"],"ITALY":["VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","HOL"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["NWG","EDI"],"FRANCE":["BRE","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC","PIE","NTH","MAO","PAR","TYR"],"GERMANY":["RUH","MUN"],"ITALY":["WES"],"RUSSIA":["SEV","BUD","RUM","GAL","LVN","FIN","SKA","STP","NWY","SWE","MOS","PRU","DEN","BER","SIL","KIE","HEL","WAR","BAR","HOL","UKR","VIE"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","ION","GRE","NAP","TYS","ROM","CON","ADR","TUN","AEG","TUS","VEN","BUL","TRI","SER"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A BUR S A TYR - MUN","F CLY - NWG","F ENG - MAO","A BEL - HOL","F NTH S A BEL - HOL","F MAO - WES","A TYR - MUN","A PAR - GAS"],"GERMANY":["A MUN H"],"ITALY":[],"RUSSIA":["F SEV S A RUM","A RUM S A BUD","A BUD S A VIE","A NWY - SWE","A BER - KIE","A SIL H","F HEL - DEN","F BAR - NWG","A UKR H","A HOL S A BEL","A WAR - GAL","F STP\/NC - NWY","A VIE S A BUD"],"TURKEY":["F BLA H","A GRE - SER","F ADR S A VEN","F AEG - ION","F TUN - WES","F TUS - PIE","A TRI - TYR","A SER - TRI","A BUL H","A VEN H"]},"results":{"F EDI":[],"A BUR":[],"F CLY":["bounce"],"F ENG":["bounce"],"A BEL":[],"F NTH":[],"F MAO":["bounce"],"A PAR":[],"A TYR":[],"A MUN":["dislodged"],"F SEV":[],"A BUD":[],"A RUM":[],"A NWY":[],"A BER":[],"A SIL":[],"F HEL":[],"F BAR":["bounce"],"A HOL":["void","dislodged"],"A UKR":[],"F STP\/NC":[],"A WAR":[],"A VIE":[],"F BLA":[],"A GRE":[],"F ADR":[],"F TUN":["bounce"],"F AEG":[],"F TUS":[],"A VEN":[],"A BUL":[],"A TRI":[],"A SER":[]},"messages":[{"sender":"FRANCE","recipient":"RUSSIA","time_sent":7870,"phase":"F1909M","message":"is turkey gonna draw though?"},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":8006,"phase":"F1909M","message":"Yes"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":8049,"phase":"F1909M","message":"Plz draw"},{"sender":"FRANCE","recipient":"RUSSIA","time_sent":8056,"phase":"F1909M","message":"why hasnt he yet? if you get him to then ill draw aswell"}]},{"name":"F1909R","state":{"timestamp":1537459329586313,"zobrist_hash":"641060405234372959","note":"","name":"F1909R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F ENG","F NTH","F MAO","A HOL","A GAS","A MUN"],"GERMANY":["*A MUN"],"ITALY":[],"RUSSIA":["F SEV","A BUD","A RUM","A SIL","F BAR","A UKR","A VIE","A SWE","A KIE","F DEN","F NWY","A GAL","*A HOL"],"TURKEY":["F BLA","F ADR","F TUN","A VEN","A BUL","A SER","F ION","F PIE","A TYR","A TRI"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL"],"GERMANY":["MUN"],"ITALY":["VEN"],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","HOL"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["NWG","EDI"],"FRANCE":["BRE","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC","NTH","MAO","PAR","HOL","GAS","MUN"],"GERMANY":["RUH"],"ITALY":["WES"],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","BER","SIL","HEL","WAR","BAR","UKR","VIE","SWE","KIE","DEN","NWY","GAL"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","NAP","TYS","ROM","CON","ADR","TUN","AEG","TUS","VEN","BUL","SER","ION","PIE","TYR","TRI"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":["A MUN D"],"ITALY":[],"RUSSIA":["A HOL R RUH"],"TURKEY":[]},"results":{"A MUN":["disband"],"A HOL":[]},"messages":[]},{"name":"W1909A","state":{"timestamp":1537459329588034,"zobrist_hash":"3311359623530045999","note":"","name":"W1909A","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F ENG","F NTH","F MAO","A HOL","A GAS","A MUN"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV","A BUD","A RUM","A SIL","F BAR","A UKR","A VIE","A SWE","A KIE","F DEN","F NWY","A GAL","A RUH"],"TURKEY":["F BLA","F ADR","F TUN","A VEN","A BUL","A SER","F ION","F PIE","A TYR","A TRI"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL","MUN","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["NWG","EDI"],"FRANCE":["BRE","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC","NTH","MAO","PAR","HOL","GAS","MUN"],"GERMANY":[],"ITALY":["WES"],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","BER","SIL","HEL","WAR","BAR","UKR","VIE","SWE","KIE","DEN","NWY","GAL","RUH"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","NAP","TYS","ROM","CON","ADR","TUN","AEG","TUS","VEN","BUL","SER","ION","PIE","TYR","TRI"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":2,"homes":["BRE","MAR","PAR"]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":-1,"homes":[]},"TURKEY":{"count":1,"homes":["ANK","CON","SMY"]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["F MAR B","A PAR B"],"GERMANY":[],"ITALY":[],"RUSSIA":["A UKR D"],"TURKEY":["F SMY B"]},"results":{"F MAR":[""],"A PAR":[""],"A UKR":[""],"F SMY":[""]},"messages":[{"sender":"FRANCE","recipient":"GLOBAL","time_sent":8188,"phase":"W1909A","message":"turkey are you going to draw?"},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":8244,"phase":"W1909A","message":"Draw draw draw draw draw"}]},{"name":"S1910M","state":{"timestamp":1537459329600454,"zobrist_hash":"8931551963878019968","note":"","name":"S1910M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F ENG","F NTH","F MAO","A HOL","A GAS","A MUN","F MAR","A PAR"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV","A BUD","A RUM","A SIL","F BAR","A VIE","A SWE","A KIE","F DEN","F NWY","A GAL","A RUH"],"TURKEY":["F BLA","F ADR","F TUN","A VEN","A BUL","A SER","F ION","F PIE","A TYR","A TRI","F SMY"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL","MUN","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":["BOH"],"ENGLAND":["NWG","EDI"],"FRANCE":["BRE","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","BEL","PIC","NTH","MAO","PAR","HOL","GAS","MUN"],"GERMANY":[],"ITALY":["WES"],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","BER","SIL","HEL","WAR","BAR","UKR","VIE","SWE","KIE","DEN","NWY","GAL","RUH"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","NAP","TYS","ROM","CON","ADR","TUN","AEG","TUS","VEN","BUL","SER","ION","PIE","TYR","TRI"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A BUR S A MUN","F CLY - NWG","F ENG - BEL","F NTH - DEN","F MAO - WES","A HOL - KIE","A GAS - MAR","A MUN S A HOL - KIE","A PAR - PIC","F MAR - LYO"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV H","A RUM S A BUD","A BUD S A VIE","A SIL - BER","F BAR - NWG","A VIE S A BUD","A GAL - BOH","A SWE - DEN","A KIE S A SIL - BER","F DEN - HEL","F NWY S F BAR - NWG","A RUH - HOL"],"TURKEY":["F BLA H","F ADR - APU","F TUN - WES","A BUL S A SER","A VEN - PIE","F PIE - LYO","A SER S A TRI","A TRI S A TYR","F ION - TYS","A TYR - MUN","F SMY - AEG"]},"results":{"F EDI":[],"A BUR":[],"F CLY":["bounce"],"F ENG":[],"F NTH":["bounce"],"F MAO":["bounce"],"A HOL":["bounce"],"A GAS":["bounce"],"A MUN":["cut"],"F MAR":["bounce"],"A PAR":[],"F SEV":[],"A BUD":[],"A RUM":[],"A SIL":[],"F BAR":[],"A VIE":[],"A SWE":["bounce"],"A KIE":["cut"],"F DEN":[],"F NWY":[],"A GAL":[],"A RUH":["bounce"],"F BLA":[],"F ADR":[],"F TUN":["bounce"],"A VEN":["bounce"],"A BUL":[],"A SER":[],"F ION":[],"F PIE":["bounce"],"A TYR":["bounce"],"A TRI":["void"],"F SMY":[]},"messages":[{"sender":"RUSSIA","recipient":"TURKEY","time_sent":8569,"phase":"S1910M","message":"Draw"},{"sender":"RUSSIA","recipient":"FRANCE","time_sent":8634,"phase":"S1910M","message":"I don't understand this he won't answer me he won't draw"}]},{"name":"F1910M","state":{"timestamp":1537459329613219,"zobrist_hash":"9153973274398142941","note":"","name":"F1910M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F NTH","F MAO","A HOL","A GAS","A MUN","F MAR","F BEL","A PIC"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A SWE","A KIE","F NWY","A RUH","A BER","F NWG","F HEL","A BOH"],"TURKEY":["F BLA","F TUN","A VEN","A BUL","A SER","F PIE","A TYR","A TRI","F APU","F TYS","F AEG"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL","MUN","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","NTH","MAO","PAR","HOL","GAS","MUN","BEL","PIC"],"GERMANY":[],"ITALY":["WES"],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","SIL","WAR","BAR","UKR","VIE","SWE","KIE","DEN","NWY","GAL","RUH","BER","NWG","HEL","BOH"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","NAP","ROM","CON","ADR","TUN","TUS","VEN","BUL","SER","ION","PIE","TYR","TRI","APU","TYS","AEG"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A BUR S A MUN","F CLY - NWG","F NTH S F CLY - NWG","F MAO - WES","A HOL - KIE","A GAS - MAR","A MUN S A HOL - KIE","F MAR - LYO","A PIC S F BEL","F BEL S F NTH"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV H","A RUM S A BUD","A BUD S A VIE","A VIE S A BUD","A SWE H","A KIE S A RUH - MUN","F NWY S F HEL - NTH","A RUH - MUN","F HEL - NTH","A BER S A RUH - MUN","F NWG S F HEL - NTH","A BOH S A RUH - MUN"],"TURKEY":["F BLA H","F TUN - WES","A BUL S A SER","A VEN - PIE","F PIE - LYO","A SER S A TRI","A TRI S A TYR","A TYR - MUN","F AEG - ION","F TYS S F PIE - LYO","F APU - NAP"]},"results":{"F EDI":[],"A BUR":[],"F CLY":["bounce"],"F NTH":["cut"],"F MAO":["bounce"],"A HOL":["bounce"],"A GAS":["bounce"],"A MUN":["cut","dislodged"],"F MAR":["bounce"],"F BEL":[],"A PIC":[],"F SEV":[],"A BUD":[],"A RUM":[],"A VIE":[],"A SWE":[],"A KIE":["cut"],"F NWY":[],"A RUH":[],"A BER":[],"F NWG":["cut"],"F HEL":["bounce"],"A BOH":[],"F BLA":[],"F TUN":["bounce"],"A VEN":[],"A BUL":[],"A SER":[],"F PIE":[],"A TYR":["bounce"],"A TRI":["void"],"F APU":[],"F TYS":[],"F AEG":[]},"messages":[]},{"name":"F1910R","state":{"timestamp":1537459329615305,"zobrist_hash":"7718889864278644728","note":"","name":"F1910R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F NTH","F MAO","A HOL","A GAS","F MAR","F BEL","A PIC","*A MUN"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A SWE","A KIE","F NWY","A BER","F NWG","F HEL","A BOH","A MUN"],"TURKEY":["F BLA","F TUN","A BUL","A SER","A TYR","A TRI","F TYS","A PIE","F LYO","F NAP","F ION"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL","MUN","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","NTH","MAO","PAR","HOL","GAS","BEL","PIC"],"GERMANY":[],"ITALY":["WES"],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","SIL","WAR","BAR","UKR","VIE","SWE","KIE","DEN","NWY","GAL","RUH","BER","NWG","HEL","BOH","MUN"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","ROM","CON","ADR","TUN","TUS","VEN","BUL","SER","TYR","TRI","APU","TYS","AEG","PIE","LYO","NAP","ION"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["A MUN R SIL"],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A MUN":[]},"messages":[]},{"name":"W1910A","state":{"timestamp":1537459329616758,"zobrist_hash":"5694870294028568635","note":"","name":"W1910A","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F NTH","F MAO","A HOL","A GAS","F MAR","F BEL","A PIC","A SIL"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A SWE","A KIE","F NWY","A BER","F NWG","F HEL","A BOH","A MUN"],"TURKEY":["F BLA","F TUN","A BUL","A SER","A TYR","A TRI","F TYS","A PIE","F LYO","F NAP","F ION"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","NTH","MAO","PAR","HOL","GAS","BEL","PIC","SIL"],"GERMANY":[],"ITALY":["WES"],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","KIE","DEN","NWY","GAL","RUH","BER","NWG","HEL","BOH","MUN"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","ROM","CON","ADR","TUN","TUS","VEN","BUL","SER","TYR","TRI","APU","TYS","AEG","PIE","LYO","NAP","ION"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":-1,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":1,"homes":["MOS","STP","WAR"]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["A SIL D"],"GERMANY":[],"ITALY":[],"RUSSIA":["A WAR B"],"TURKEY":[]},"results":{"A SIL":[""],"A WAR":[""]},"messages":[{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":9080,"phase":"W1910A","message":"CAN WE PLEASE FUCKING DRAW GODDAMIT"},{"sender":"FRANCE","recipient":"GLOBAL","time_sent":9087,"phase":"W1910A","message":"come on turkey were all bored of this game"}]},{"name":"S1911M","state":{"timestamp":1537459329629087,"zobrist_hash":"4126256491755472626","note":"","name":"S1911M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F NTH","F MAO","A HOL","A GAS","F MAR","F BEL","A PIC"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","A SWE","A KIE","F NWY","A BER","F NWG","F HEL","A BOH","A MUN","A WAR"],"TURKEY":["F BLA","F TUN","A BUL","A SER","A TYR","A TRI","F TYS","A PIE","F LYO","F NAP","F ION"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","SPA","POR","MAR","IRI","BUR","LVP","LON","CLY","ENG","NTH","MAO","PAR","HOL","GAS","BEL","PIC","SIL"],"GERMANY":[],"ITALY":["WES"],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","KIE","DEN","NWY","GAL","RUH","BER","NWG","HEL","BOH","MUN"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","ROM","CON","ADR","TUN","TUS","VEN","BUL","SER","TYR","TRI","APU","TYS","AEG","PIE","LYO","NAP","ION"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A BUR S F MAR","F CLY - NWG","F NTH S A HOL","F MAO - WES","A HOL S F BEL","A GAS - SPA","F MAR H","A PIC S A BUR","F BEL S F NTH"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV H","A RUM H","A BUD S A VIE","A VIE S A BUD","A SWE - DEN","A KIE - HOL","F NWY S F NWG - NTH","F HEL S A KIE - HOL","A BER - KIE","F NWG - NTH","A BOH S A VIE","A MUN - BUR","A WAR - SIL"],"TURKEY":["F BLA H","F TUN - WES","A BUL S A SER","A SER S A TRI","A TRI S A TYR","A TYR H","F TYS S F TUN - WES","F LYO S A PIE - MAR","A PIE - MAR","F NAP - ION","F ION - TUN"]},"results":{"F EDI":[],"A BUR":["cut"],"F CLY":["bounce"],"F NTH":["cut"],"F MAO":["bounce"],"A HOL":["cut","dislodged"],"A GAS":[],"F MAR":["dislodged"],"F BEL":[],"A PIC":[],"F SEV":[],"A BUD":[],"A RUM":[],"A VIE":[],"A SWE":[],"A KIE":[],"F NWY":[],"A BER":[],"F NWG":["bounce"],"F HEL":[],"A BOH":[],"A MUN":["bounce"],"A WAR":[],"F BLA":[],"F TUN":[],"A BUL":[],"A SER":[],"A TYR":[],"A TRI":[],"F TYS":[],"A PIE":[],"F LYO":[],"F NAP":[],"F ION":[]},"messages":[{"sender":"FRANCE","recipient":"RUSSIA","time_sent":9145,"phase":"S1911M","message":"lets go for turkey, that way we force him to draw"}]},{"name":"S1911R","state":{"timestamp":1537459329631436,"zobrist_hash":"8293939813843744517","note":"","name":"S1911R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F NTH","F MAO","F BEL","A PIC","A SPA","*A HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","F NWY","F NWG","F HEL","A BOH","A MUN","A DEN","A HOL","A KIE","A SIL"],"TURKEY":["F BLA","A BUL","A SER","A TYR","A TRI","F TYS","F LYO","F WES","A MAR","F ION","F TUN"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","POR","IRI","BUR","LVP","LON","CLY","ENG","NTH","MAO","PAR","GAS","BEL","PIC","SPA"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","RUH","BER","NWG","HEL","BOH","MUN","DEN","HOL","KIE","SIL"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","ROM","CON","ADR","TUS","VEN","BUL","SER","TYR","TRI","APU","TYS","AEG","PIE","LYO","NAP","WES","MAR","ION","TUN"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["A HOL R RUH"],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":[]},"results":{"A HOL":[],"F MAR":["disband"]},"messages":[]},{"name":"F1911M","state":{"timestamp":1537459329644026,"zobrist_hash":"6264455286534979117","note":"","name":"F1911M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F NTH","F MAO","F BEL","A PIC","A SPA","A RUH"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV","A BUD","A RUM","A VIE","F NWY","F NWG","F HEL","A BOH","A MUN","A DEN","A HOL","A KIE","A SIL"],"TURKEY":["F BLA","A BUL","A SER","A TYR","A TRI","F TYS","F LYO","F WES","A MAR","F ION","F TUN"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","POR","IRI","BUR","LVP","LON","CLY","ENG","NTH","MAO","PAR","GAS","BEL","PIC","SPA","RUH"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","BER","NWG","HEL","BOH","MUN","DEN","HOL","KIE","SIL"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","ROM","CON","ADR","TUS","VEN","BUL","SER","TYR","TRI","APU","TYS","AEG","PIE","LYO","NAP","WES","MAR","ION","TUN"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A BUR S A SPA - MAR","F CLY - NWG","F NTH S A RUH - HOL","F MAO - WES","A PIC S A BUR","F BEL S A RUH - HOL","A SPA - MAR","A RUH - HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV - ARM","A RUM H","A BUD S A RUM","A VIE S A BUD","F NWY - NTH","F HEL S F NWY - NTH","F NWG S F NWY - NTH","A BOH S A VIE","A MUN S A KIE","A SIL S A MUN","A DEN S A KIE","A HOL - BEL","A KIE S A HOL"],"TURKEY":["F BLA S A BUL","A BUL S A SER","A SER S A TRI","A TRI S A TYR","A TYR H","F TYS H","F LYO S F WES - SPA","F WES - SPA\/SC","F ION H","A MAR S F WES - SPA","F TUN - WES"]},"results":{"F EDI":[],"A BUR":[],"F CLY":["bounce"],"F NTH":["cut","dislodged"],"F MAO":["bounce"],"F BEL":[],"A PIC":[],"A SPA":[],"A RUH":[],"F SEV":[],"A BUD":[],"A RUM":[],"A VIE":[],"F NWY":[],"F NWG":["cut"],"F HEL":[],"A BOH":[],"A MUN":[],"A DEN":[],"A HOL":["bounce","dislodged"],"A KIE":["void"],"A SIL":[],"F BLA":[],"A BUL":[],"A SER":[],"A TYR":[],"A TRI":[],"F TYS":[],"F LYO":[],"F WES":[],"A MAR":["cut","dislodged"],"F ION":[],"F TUN":["bounce"]},"messages":[]},{"name":"F1911R","state":{"timestamp":1537459329646347,"zobrist_hash":"1743515476365396544","note":"","name":"F1911R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F MAO","F BEL","A PIC","A MAR","A HOL","*F NTH"],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUD","A RUM","A VIE","F NWG","F HEL","A BOH","A MUN","A DEN","A KIE","A SIL","F ARM","F NTH"],"TURKEY":["F BLA","A BUL","A SER","A TYR","A TRI","F TYS","F LYO","F ION","F TUN","F SPA\/SC","*A MAR"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","SPA","LON","LVP","BEL","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","POR","IRI","BUR","LVP","LON","CLY","ENG","MAO","PAR","GAS","BEL","PIC","RUH","MAR","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","BER","NWG","HEL","BOH","MUN","DEN","KIE","SIL","ARM","NTH"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","ROM","CON","ADR","TUS","VEN","BUL","SER","TYR","TRI","APU","TYS","AEG","PIE","LYO","NAP","WES","ION","TUN","SPA"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["F NTH R ENG"],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":["A MAR R PIE"]},"results":{"F NTH":[],"A MAR":[],"A HOL":["disband"]},"messages":[{"sender":"FRANCE","recipient":"GLOBAL","time_sent":9739,"phase":"F1911R","message":"for fucks sakes"}]},{"name":"W1911A","state":{"timestamp":1537459329647975,"zobrist_hash":"387435341550858209","note":"","name":"W1911A","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F MAO","F BEL","A PIC","A MAR","A HOL","F ENG"],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUD","A RUM","A VIE","F NWG","F HEL","A BOH","A MUN","A DEN","A KIE","A SIL","F ARM","F NTH"],"TURKEY":["F BLA","A BUL","A SER","A TYR","A TRI","F TYS","F LYO","F ION","F TUN","F SPA\/SC","A PIE"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","LON","LVP","BEL","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN","SPA"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","POR","IRI","BUR","LVP","LON","CLY","MAO","PAR","GAS","BEL","PIC","RUH","MAR","HOL","ENG"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","BER","NWG","HEL","BOH","MUN","DEN","KIE","SIL","ARM","NTH"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","ROM","CON","ADR","TUS","VEN","BUL","SER","TYR","TRI","APU","TYS","AEG","LYO","NAP","WES","ION","TUN","SPA","PIE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":1,"homes":["MOS","SEV","STP","WAR"]},"TURKEY":{"count":1,"homes":["ANK","CON","SMY"]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":["F SEV B"],"TURKEY":["A CON B"]},"results":{"F SEV":[""],"A CON":[""]},"messages":[{"sender":"FRANCE","recipient":"TURKEY","time_sent":9771,"phase":"W1911A","message":"turkey y wont u draw?"},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":9883,"phase":"W1911A","message":"I swear to god as soon as I find out who you are......"},{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":9900,"phase":"W1911A","message":"I swear to god as soon as I find out who you are......"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":9919,"phase":"W1911A","message":"Ok but the France is bed!"},{"sender":"FRANCE","recipient":"GLOBAL","time_sent":9936,"phase":"W1911A","message":"turkey are you some chinese little nerd or something?"}]},{"name":"S1912M","state":{"timestamp":1537459329661740,"zobrist_hash":"6610247329564000625","note":"","name":"S1912M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F MAO","F BEL","A PIC","A MAR","A HOL","F ENG"],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUD","A RUM","A VIE","F NWG","F HEL","A BOH","A MUN","A DEN","A KIE","A SIL","F ARM","F NTH","F SEV"],"TURKEY":["F BLA","A BUL","A SER","A TYR","A TRI","F TYS","F LYO","F ION","F TUN","F SPA\/SC","A PIE","A CON"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","LON","LVP","BEL","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN","SPA"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","POR","IRI","BUR","LVP","LON","CLY","MAO","PAR","GAS","BEL","PIC","RUH","MAR","HOL","ENG"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","BER","NWG","HEL","BOH","MUN","DEN","KIE","SIL","ARM","NTH"],"TURKEY":["ANK","SMY","BLA","EAS","ALB","GRE","ROM","CON","ADR","TUS","VEN","BUL","SER","TYR","TRI","APU","TYS","AEG","LYO","NAP","WES","ION","TUN","SPA","PIE"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A BUR - MAR","F CLY - NWG","F MAO S A MAR - SPA","A PIC S F BEL","F BEL S A HOL","A MAR - SPA","A HOL S F BEL","F ENG - LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["A RUM H","A BUD S A RUM","A VIE S A BUD","F HEL S A DEN - KIE","F NWG S F NTH","A BOH S A VIE","A MUN S A KIE - RUH","A SIL S A MUN","A DEN - KIE","A KIE - RUH","F ARM S F SEV - BLA","F NTH S F NWG","F SEV - BLA"],"TURKEY":["F BLA S A BUL","A BUL S A SER","A SER S A TRI","A TRI S A TYR","A TYR H","F TYS H","F LYO S A PIE - MAR","F ION H","F TUN - WES","F SPA\/SC S A PIE - MAR","A PIE - MAR","A CON H"]},"results":{"F EDI":[],"A BUR":["bounce"],"F CLY":["bounce"],"F MAO":[],"F BEL":[],"A PIC":[],"A MAR":[],"A HOL":[],"F ENG":[],"A BUD":[],"A RUM":[],"A VIE":[],"F NWG":["cut"],"F HEL":[],"A BOH":[],"A MUN":[],"A DEN":[],"A KIE":[],"A SIL":[],"F ARM":[],"F NTH":[],"F SEV":[],"F BLA":["cut","dislodged"],"A BUL":[],"A SER":[],"A TYR":[],"A TRI":[],"F TYS":[],"F LYO":[],"F ION":[],"F TUN":[],"F SPA\/SC":["cut","dislodged"],"A PIE":[],"A CON":[]},"messages":[{"sender":"RUSSIA","recipient":"TURKEY","time_sent":10000,"phase":"S1912M","message":"Plz draw"},{"sender":"FRANCE","recipient":"GLOBAL","time_sent":10016,"phase":"S1912M","message":"russia y dont you think about my proposal"},{"sender":"TURKEY","recipient":"FRANCE","time_sent":10080,"phase":"S1912M","message":"In next atumn!"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":10107,"phase":"S1912M","message":"In neext atumn"},{"sender":"RUSSIA","recipient":"TURKEY","time_sent":10177,"phase":"S1912M","message":"It doesn't matter!"}]},{"name":"S1912R","state":{"timestamp":1537459329664126,"zobrist_hash":"7209563349558220437","note":"","name":"S1912R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F MAO","F BEL","A PIC","A HOL","A SPA","F LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUD","A RUM","A VIE","F NWG","F HEL","A BOH","A MUN","A SIL","F ARM","F NTH","A KIE","A RUH","F BLA"],"TURKEY":["A BUL","A SER","A TYR","A TRI","F TYS","F LYO","F ION","A CON","F WES","A MAR","*F BLA","*F SPA\/SC"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","LON","LVP","BEL","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN","SPA"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","POR","IRI","BUR","LVP","CLY","MAO","PAR","GAS","BEL","PIC","HOL","ENG","SPA","LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","BER","NWG","HEL","BOH","MUN","DEN","SIL","ARM","NTH","KIE","RUH","BLA"],"TURKEY":["ANK","SMY","EAS","ALB","GRE","ROM","CON","ADR","TUS","VEN","BUL","SER","TYR","TRI","APU","TYS","AEG","LYO","NAP","ION","TUN","PIE","WES","MAR"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":["F BLA R ANK","F SPA\/SC R POR"]},"results":{"F BLA":[],"F SPA\/SC":[]},"messages":[{"sender":"FRANCE","recipient":"TURKEY","time_sent":10352,"phase":"S1912R","message":"please just do it now!"}]},{"name":"F1912M","state":{"timestamp":1537459329677031,"zobrist_hash":"6060696874480043574","note":"","name":"F1912M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F MAO","F BEL","A PIC","A HOL","A SPA","F LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUD","A RUM","A VIE","F NWG","F HEL","A BOH","A MUN","A SIL","F ARM","F NTH","A KIE","A RUH","F BLA"],"TURKEY":["A BUL","A SER","A TYR","A TRI","F TYS","F LYO","F ION","A CON","F WES","A MAR","F ANK","F POR"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","LON","LVP","BEL","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN","SPA"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","IRI","BUR","LVP","CLY","MAO","PAR","GAS","BEL","PIC","HOL","ENG","SPA","LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","BER","NWG","HEL","BOH","MUN","DEN","SIL","ARM","NTH","KIE","RUH","BLA"],"TURKEY":["SMY","EAS","ALB","GRE","ROM","CON","ADR","TUS","VEN","BUL","SER","TYR","TRI","APU","TYS","AEG","LYO","NAP","ION","TUN","PIE","WES","MAR","ANK","POR"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A BUR S F BEL","F CLY - NWG","F MAO - POR","A PIC S A BUR","F BEL S F LON - NTH","A HOL - RUH","A SPA S F MAO - POR","F LON - NTH"],"GERMANY":[],"ITALY":[],"RUSSIA":["A RUM - SER","A BUD S A VIE - TRI","A VIE - TRI","F HEL S A KIE - HOL","F NWG - EDI","A BOH - TYR","A MUN S A BOH - TYR","A SIL - BER","F ARM - ANK","F NTH S F NWG - EDI","F BLA S F ARM - ANK","A KIE - HOL","A RUH - BEL"],"TURKEY":["A BUL S A CON","A SER S A TRI","A TRI S A TYR","A TYR H","F TYS H","F LYO S A MAR","F ION - TUN","A CON S F ANK","F WES - SPA\/SC","A MAR H","F ANK S A CON","F POR S F WES - SPA"]},"results":{"F EDI":[],"A BUR":[],"F CLY":["bounce"],"F MAO":["bounce"],"F BEL":["cut"],"A PIC":[],"A HOL":["bounce","dislodged"],"A SPA":["cut"],"F LON":["bounce"],"A BUD":[],"A RUM":["bounce"],"A VIE":[],"F NWG":["bounce"],"F HEL":[],"A BOH":[],"A MUN":[],"A SIL":[],"F ARM":["bounce"],"F NTH":["cut"],"A KIE":[],"A RUH":["bounce"],"F BLA":[],"A BUL":[],"A SER":["cut"],"A TYR":["dislodged"],"A TRI":["cut","dislodged"],"F TYS":[],"F LYO":[],"F ION":[],"A CON":[],"F WES":["bounce"],"A MAR":[],"F ANK":["cut"],"F POR":["cut"]},"messages":[]},{"name":"F1912R","state":{"timestamp":1537459329679358,"zobrist_hash":"3417588792576588076","note":"","name":"F1912R","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F MAO","F BEL","A PIC","A SPA","F LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUD","A RUM","F NWG","F HEL","A MUN","F ARM","F NTH","A RUH","F BLA","A TRI","A TYR","A BER","A HOL"],"TURKEY":["A BUL","A SER","F TYS","F LYO","A CON","F WES","A MAR","F ANK","F POR","F TUN","*A TYR","*A TRI"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","MAR","PAR","POR","LON","LVP","BEL","HOL"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TRI","TUN","NAP","ROM","VEN","SPA"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","IRI","BUR","LVP","CLY","MAO","PAR","GAS","BEL","PIC","ENG","SPA","LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","NWG","HEL","BOH","MUN","DEN","SIL","ARM","NTH","KIE","RUH","BLA","TRI","TYR","BER","HOL"],"TURKEY":["SMY","EAS","ALB","GRE","ROM","CON","ADR","TUS","VEN","BUL","SER","APU","TYS","AEG","LYO","NAP","ION","PIE","WES","MAR","ANK","POR","TUN"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":[],"GERMANY":[],"ITALY":[],"RUSSIA":[],"TURKEY":["A TRI R ALB","A TYR R VEN"]},"results":{"A TYR":[],"A TRI":[],"A HOL":["disband"]},"messages":[{"sender":"RUSSIA","recipient":"GLOBAL","time_sent":10845,"phase":"F1912R","message":"Oh now you want to draw huh......."},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":10855,"phase":"F1912R","message":"I pressd drow"},{"sender":"TURKEY","recipient":"RUSSIA","time_sent":10862,"phase":"F1912R","message":"I pressd drow"}]},{"name":"W1912A","state":{"timestamp":1537459329680937,"zobrist_hash":"6117580600568058448","note":"","name":"W1912A","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F CLY","F MAO","F BEL","A PIC","A SPA","F LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUD","A RUM","F NWG","F HEL","A MUN","F ARM","F NTH","A RUH","F BLA","A TRI","A TYR","A BER","A HOL"],"TURKEY":["A BUL","A SER","F TYS","F LYO","A CON","F WES","A MAR","F ANK","F POR","F TUN","A ALB","A VEN"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","PAR","LON","LVP","BEL","SPA"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN","HOL","TRI"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TUN","NAP","ROM","VEN","MAR","POR"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","IRI","BUR","LVP","CLY","MAO","PAR","GAS","BEL","PIC","ENG","SPA","LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","NWG","HEL","BOH","MUN","DEN","SIL","ARM","NTH","KIE","RUH","BLA","TRI","TYR","BER","HOL"],"TURKEY":["SMY","EAS","GRE","ROM","CON","ADR","TUS","BUL","SER","APU","TYS","AEG","LYO","NAP","ION","PIE","WES","MAR","ANK","POR","TUN","ALB","VEN"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":-1,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":2,"homes":["MOS","SEV","STP","WAR"]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":[],"FRANCE":["F CLY D"],"GERMANY":[],"ITALY":[],"RUSSIA":["A SEV B","A WAR B"],"TURKEY":[]},"results":{"F CLY":[""],"A SEV":[""],"A WAR":[""]},"messages":[{"sender":"TURKEY","recipient":"GLOBAL","time_sent":10949,"phase":"W1912A","message":"England pressd not dow yet!"}]},{"name":"S1913M","state":{"timestamp":1537459329692613,"zobrist_hash":"5983826997068141425","note":"","name":"S1913M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F MAO","F BEL","A PIC","A SPA","F LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUD","A RUM","F NWG","F HEL","A MUN","F ARM","F NTH","A RUH","F BLA","A TRI","A TYR","A BER","A HOL","A SEV","A WAR"],"TURKEY":["A BUL","A SER","F TYS","F LYO","A CON","F WES","A MAR","F ANK","F POR","F TUN","A ALB","A VEN"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","PAR","LON","LVP","BEL","SPA"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN","HOL","TRI"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TUN","NAP","ROM","VEN","MAR","POR"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","IRI","BUR","LVP","CLY","MAO","PAR","GAS","BEL","PIC","ENG","SPA","LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","NWG","HEL","BOH","MUN","DEN","SIL","ARM","NTH","KIE","RUH","BLA","TRI","TYR","BER","HOL"],"TURKEY":["SMY","EAS","GRE","ROM","CON","ADR","TUS","BUL","SER","APU","TYS","AEG","LYO","NAP","ION","PIE","WES","MAR","ANK","POR","TUN","ALB","VEN"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":["F EDI H"],"FRANCE":["A BUR H","F MAO H","A PIC H","F BEL H","A SPA H","F LON H"],"GERMANY":[],"ITALY":[],"RUSSIA":["A RUM S A TRI - SER","A BUD S A TRI - SER","F HEL - NTH","F NWG - CLY","A MUN - BUR","F ARM - SEV","F NTH - ENG","F BLA - BUL\/EC","A RUH S A HOL - BEL","A HOL - BEL","A BER - KIE","A TYR - VEN","A TRI S A BUD - SER","A SEV - ARM","A WAR - SIL"],"TURKEY":["A BUL H","A SER H","F TYS H","F LYO H","A CON H","F WES H","A MAR H","F ANK H","F POR H","F TUN H","A VEN H","A ALB H"]},"results":{"F EDI":[],"A BUR":[],"F MAO":[],"F BEL":["dislodged"],"A PIC":[],"A SPA":[],"F LON":[],"A BUD":["void"],"A RUM":["void"],"F NWG":[],"F HEL":[],"A MUN":["bounce"],"F ARM":["bounce"],"F NTH":[],"A RUH":[],"F BLA":["bounce"],"A TRI":["void"],"A TYR":["bounce"],"A BER":[],"A HOL":[],"A SEV":["bounce"],"A WAR":[],"A BUL":[],"A SER":[],"F TYS":[],"F LYO":[],"A CON":[],"F WES":[],"A MAR":[],"F ANK":[],"F POR":[],"F TUN":[],"A ALB":[],"A VEN":[]},"messages":[]},{"name":"F1913M","state":{"timestamp":1537459329701104,"zobrist_hash":"6490709952619879683","note":"","name":"F1913M","units":{"AUSTRIA":[],"ENGLAND":["F EDI"],"FRANCE":["A BUR","F MAO","A PIC","A SPA","F LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["A BUD","A RUM","A MUN","F ARM","A RUH","F BLA","A TRI","A TYR","A SEV","F CLY","F NTH","F ENG","A KIE","A BEL","A SIL"],"TURKEY":["A BUL","A SER","F TYS","F LYO","A CON","F WES","A MAR","F ANK","F POR","F TUN","A ALB","A VEN"]},"centers":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","PAR","LON","LVP","BEL","SPA"],"GERMANY":[],"ITALY":[],"RUSSIA":["MOS","SEV","STP","WAR","BUD","RUM","VIE","NWY","SWE","BER","KIE","DEN","MUN","HOL","TRI"],"TURKEY":["ANK","CON","SMY","BUL","GRE","SER","TUN","NAP","ROM","VEN","MAR","POR"]},"homes":{"AUSTRIA":["BUD","TRI","VIE"],"ENGLAND":["EDI","LON","LVP"],"FRANCE":["BRE","MAR","PAR"],"GERMANY":["BER","KIE","MUN"],"ITALY":["NAP","ROM","VEN"],"RUSSIA":["MOS","SEV","STP","WAR"],"TURKEY":["ANK","CON","SMY"]},"influence":{"AUSTRIA":[],"ENGLAND":["EDI"],"FRANCE":["BRE","IRI","BUR","LVP","MAO","PAR","GAS","PIC","SPA","LON"],"GERMANY":[],"ITALY":[],"RUSSIA":["SEV","BUD","RUM","LVN","FIN","SKA","STP","MOS","PRU","WAR","BAR","UKR","VIE","SWE","NWY","GAL","NWG","HEL","BOH","MUN","DEN","ARM","RUH","BLA","TRI","TYR","BER","HOL","CLY","NTH","ENG","KIE","BEL","SIL"],"TURKEY":["SMY","EAS","GRE","ROM","CON","ADR","TUS","BUL","SER","APU","TYS","AEG","LYO","NAP","ION","PIE","WES","MAR","ANK","POR","TUN","ALB","VEN"]},"civil_disorder":{"AUSTRIA":0,"ENGLAND":0,"FRANCE":0,"GERMANY":0,"ITALY":0,"RUSSIA":0,"TURKEY":0},"builds":{"AUSTRIA":{"count":0,"homes":[]},"ENGLAND":{"count":0,"homes":[]},"FRANCE":{"count":0,"homes":[]},"GERMANY":{"count":0,"homes":[]},"ITALY":{"count":0,"homes":[]},"RUSSIA":{"count":0,"homes":[]},"TURKEY":{"count":0,"homes":[]}},"game_id":"0021f2cf","map":"standard","rules":[]},"orders":{"AUSTRIA":[],"ENGLAND":null,"FRANCE":null,"GERMANY":[],"ITALY":[],"RUSSIA":null,"TURKEY":null},"results":{},"messages":[]}]}
\ No newline at end of file diff --git a/diplomacy/tests/network/__init__.py b/diplomacy/tests/network/__init__.py new file mode 100644 index 0000000..4f2769f --- /dev/null +++ b/diplomacy/tests/network/__init__.py @@ -0,0 +1,16 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== diff --git a/diplomacy/tests/network/run_real_game.py b/diplomacy/tests/network/run_real_game.py new file mode 100644 index 0000000..a9800b6 --- /dev/null +++ b/diplomacy/tests/network/run_real_game.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Run tests from diplomacy.tests.network.test_real_game to test games in a real environment. + Each test run a game and checks game messages and phases against an expected game data file. + Current tested gama data files are JSON files located into folder diplomacy/tests/network: + 1.json + 2.json + 3.json + Need a local diplomacy server running. You must specify this server port using parameter --port=<server_port>. + To run all tests: + python -m diplomacy.tests.network.run_real_game --port=<server_port> + To run a specific test (e.g. 2.json, or 2.json and 1.json): + python -m diplomacy.tests.network.run_real_game --cases=20 --port=<server_port> + python -m diplomacy.tests.network.run_real_game --cases=15,20 --port=<server_port> + For help: + python -m diplomacy.tests.network.run_real_game --help +""" +import argparse +from tornado import gen +from tornado.ioloop import IOLoop + +from diplomacy.tests.network import test_real_game + +def launch_case(case_name, port, io_loop): + """ Launch a game case. """ + case_data = test_real_game.CaseData(case_name, port=port) + case_data.io_loop = io_loop + return test_real_game.main(case_data) + +def main(): + """ Main function for this module. Load and run tests. + Each test run a game and checks game messages and phases against an expected game data file. + Current tested gama data files are JSON files located into folder diplomacy/tests/network. + """ + parser = argparse.ArgumentParser(description='Run test cases against an external server to connect.') + parser.add_argument('--port', type=int, required=True, + help='run on the given port (required)') + parser.add_argument('--cases', action='append', + help="Run given cases. " + "Each case <C> must match a test case file <C>.json located in diplomacy.tests.network. " + "If not provided, all available cases are run.") + args = parser.parse_args() + io_loop = IOLoop() + io_loop.make_current() + + @gen.coroutine + def run(): + """ Run all tests consecutively in one call. """ + tests = set(args.cases) if args.cases else {'1', '2', '3'} + for test_case in list(sorted(tests)): + yield launch_case('%s.json' % test_case, args.port, io_loop) + + io_loop.run_sync(run) + + +if __name__ == '__main__': + main() diff --git a/diplomacy/tests/network/test_real_game.py b/diplomacy/tests/network/test_real_game.py new file mode 100644 index 0000000..a16e4f6 --- /dev/null +++ b/diplomacy/tests/network/test_real_game.py @@ -0,0 +1,605 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Test server game in real environment with test data in files `{15, 20, 23}.json`. """ +# pylint: disable=unused-argument +import logging +import os +import random + +from tornado import gen +from tornado.concurrent import Future +from tornado.ioloop import IOLoop + +import ujson as json + +from diplomacy.client.connection import connect +from diplomacy.server.server import Server +from diplomacy.engine.game import Game +from diplomacy.engine.map import Map +from diplomacy.engine.message import GLOBAL, Message as EngineMessage +from diplomacy.utils import common, exceptions, constants, strings + +LOGGER = logging.getLogger('diplomacy.tests.network.test_real_game') + +DEFAULT_HOSTNAME = 'localhost' + +DEFAULT_PORT = random.randint(9000, 10000) + +class ExpectedPhase(): + """ Helper class to manage data from an expected phase. """ + __slots__ = ['name', 'state', 'orders', 'messages'] + + def __init__(self, json_phase): + """ Initialize expected phase. + :param json_phase: JSON dict representing a phase. Expected fields: name, state, orders, messages. + """ + self.name = json_phase['name'] + self.state = json_phase['state'] + self.orders = json_phase['orders'] + self.messages = [EngineMessage(**json_message) for json_message in json_phase['messages']] + + self.messages.sort(key=lambda msg: msg.time_sent) + + def get_power_orders(self, power_name): + """ Return expected orders for given power name. """ + return self.orders[power_name] + + def get_power_related_messages(self, power_name): + """ Return expected messages for given power name. """ + return [message for message in self.messages + if message.sender == power_name or message.recipient in (power_name, GLOBAL)] + +class ExpectedMessages(): + """ Expected list of messages sent and received by a power name. """ + __slots__ = ['power_name', 'messages', 'next_messages_to_send'] + + def __init__(self, power_name, messages): + """ Initialize the expected messages. + :param power_name: power name which exchanges these messages + :param messages: messages exchanged + """ + self.power_name = power_name + self.messages = messages # type: [EngineMessage] + self.next_messages_to_send = [] + + def has_messages_to_receive(self): + """ Return True if messages list still contains messages to receive. """ + return any(message.sender != self.power_name for message in self.messages) + + def has_messages_to_send(self): + """ Return True if messages list still contains messages to send. """ + return any(message.sender == self.power_name for message in self.messages) + + def move_forward(self): + """ Move next messages to send from messages list to sending queue (self.next_messages_to_send). """ + self.next_messages_to_send.clear() + if self.messages: + if self.messages[0].sender != self.power_name: + # First message in stack is a message to receive. We cannot send any message + # until all messages to receive at top of stack were indeed received. + return + next_message_to_receive = len(self.messages) + for index, message in enumerate(self.messages): + if message.sender != self.power_name: + next_message_to_receive = index + break + self.next_messages_to_send.extend(self.messages[:next_message_to_receive]) + del self.messages[:next_message_to_receive] + +class ExpectedData(): + """ Expected data for a power in a game. """ + + __slots__ = ['messages', 'phases', '__phase_index', 'playing'] + + def __init__(self, power_name, phases, phase_index): + """ Initialize expected data for a game power. + :param power_name: name of power for which those data are expected. + :param phases: list of expected phases. + :param phase_index: index of current expected phase in given phases. + :type power_name: str + :type phases: list[ExpectedPhase] + :type phase_index: int + """ + self.messages = ExpectedMessages(power_name, phases[phase_index].get_power_related_messages(power_name)) + self.phases = phases + self.__phase_index = phase_index + self.playing = False + + power_name = property(lambda self: self.messages.power_name) + phase_index = property(lambda self: self.__phase_index) + expected_phase = property(lambda self: self.phases[self.__phase_index]) + + def move_forward(self): + """ Move to next expected phase. """ + self.__phase_index += 1 + if self.__phase_index < len(self.phases): + self.messages = ExpectedMessages( + self.messages.power_name, self.phases[self.__phase_index].get_power_related_messages(self.power_name)) + +class CaseData(): + """ Helper class to store test data. """ + FILE_FOLDER_NAME = os.path.abspath(os.path.dirname(__file__)) + + def __init__(self, case_file_name, hostname=DEFAULT_HOSTNAME, port=DEFAULT_PORT): + """ Initialize game test. + :param case_file_name: File name of JSON file containing expected game data. + JSON file must be located in folder FILE_FOLDER_NAME. + :param hostname: hostname to use to load server. + :param port: port to use to load server. + """ + full_file_path = os.path.join(self.FILE_FOLDER_NAME, case_file_name) + with open(full_file_path, 'rb') as file: + data = json.load(file) + self.case_name = case_file_name + self.map_name = data['map'] + self.phases = [ExpectedPhase(json_phase) for json_phase in data['phases']] + self.rules = set(data['rules']) + self.rules.add('POWER_CHOICE') + self.rules.add('REAL_TIME') + + self.test_server = None + self.io_loop = None # type: IOLoop + self.connection = None + self.admin_channel = None + self.admin_game = None + self.user_games = {} + self.future_games_ended = {} # type: dict{str, Future} + + self.hostname = hostname + self.port = port + + def terminate_game(self, power_name): + """ Tell Tornado that a power game is finished. """ + self.future_games_ended[power_name].set_result(None) + + @gen.coroutine + def on_power_phase_update(self, game, notification=None): + """ User game notification callback for game phase updated. + :param game: game + :param notification: notification + :type game: NetworkGame + :type notification: diplomacy.communication.notifications.GameProcessed | None + """ + print('We changed phase for power', game.power.name) + expected_data = game.data # type: ExpectedData + expected_data.move_forward() + if expected_data.phase_index >= len(expected_data.phases): + assert expected_data.phase_index == len(expected_data.phases) + self.terminate_game(game.data.power_name) + print('Game fully terminated at phase', game.phase) + else: + yield verify_current_phase(game) + + @gen.coroutine + def on_power_state_update(self, game, notification): + """ User game notification callback for game state update. + :param game: game + :param notification: notification + :type game: NetworkGame + :type notification: diplomacy.communication.notifications.GamePhaseUpdate + """ + if notification.phase_data_type == strings.PHASE: + yield self.on_power_phase_update(game, None) + +@gen.coroutine +def send_messages_if_needed(game, expected_messages): + """ Take messages to send in top of given messages list and send them. + :param game: a NetworkGame object. + :param expected_messages: an instance of ExpectedMessages. + :type game: NetworkGame + :type expected_messages: ExpectedMessages + """ + power_name = game.power.name + + if expected_messages.messages: + expected_messages.move_forward() + for message in expected_messages.next_messages_to_send: + if message.recipient == GLOBAL: + print('%s/sending global message (time %d)' % (power_name, message.time_sent)) + yield game.send_game_message(message=game.new_global_message(message.message)) + print('%s/sent global message (time %d)' % (power_name, message.time_sent)) + else: + print('%s/sending message to %s (time %d)' % (power_name, message.recipient, message.time_sent)) + yield game.send_game_message(message=game.new_power_message( + message.recipient, message.message)) + print('%s/sent message to %s (time %d)' % (power_name, message.recipient, message.time_sent)) + expected_messages.next_messages_to_send.clear() + +@gen.coroutine +def send_current_orders(game): + """ Send expected orders for current phase. + :param game: a Network game object. + :type game: NetworkGame + """ + expected_data = game.data # type: ExpectedData + orders_to_send = expected_data.expected_phase.get_power_orders(expected_data.power_name) + if orders_to_send is None: + orders_to_send = [] + print('%s/sending %d orders for phase %s: %s' % (expected_data.power_name, len(orders_to_send), + expected_data.expected_phase.name, orders_to_send)) + yield game.set_orders(orders=orders_to_send) + print('%s/sent orders for phase %s' % (expected_data.power_name, expected_data.expected_phase.name)) + +def on_message_received(game, notification): + """ User game notification callback for messages received. + :param game: a NetworkGame object, + :param notification: a notification received by this game. + :type game: NetworkGame + :type notification: diplomacy.communication.notifications.GameMessageReceived + """ + power_name = game.power.name + messages = game.data.messages # type: ExpectedMessages + + if not messages.has_messages_to_receive(): + raise AssertionError('%s/should not receive more messages.' % power_name) + + power_from = notification.message.sender + index_found = None + for index, expected_message in enumerate(messages.messages): + if expected_message.recipient == power_from: + raise AssertionError( + '%s/there are still messages to send to %s (%d) before receiving messages from him. Received: %s' + % (power_name, power_from, expected_message.time_sent, notification.message.message)) + elif expected_message.sender == power_from: + if notification.message.is_global(): + if not (expected_message.recipient == GLOBAL + and expected_message.message == notification.message.message): + raise AssertionError( + '%s/first expected message from %s does not match received global message: %s' + % (power_name, power_from, notification.message.message)) + else: + if not (expected_message.recipient == notification.message.recipient + and expected_message.message == notification.message.message): + raise AssertionError( + '%s/first expected message from %s does not match received power message: to %s: %s' + % (power_name, power_from, notification.message.recipient, notification.message.message)) + index_found = index + break + + if index_found is None: + raise AssertionError('%s/Received unknown message from %s to %s: %s' % ( + power_name, notification.message.sender, notification.message.recipient, notification.message.message)) + + expected_message = messages.messages.pop(index_found) + + print('%s/checked message (time %d)' % (power_name, expected_message.time_sent)) + +def on_admin_game_phase_update(admin_game, notification=None): + """ Admin game notification callback for game phase update. + :param admin_game: admin game + :param notification: notification + :type admin_game: NetworkGame + :type notification: diplomacy.communication.notifications.GameProcessed | None + """ + assert admin_game.is_omniscient_game() + expected_data = admin_game.data # type: ExpectedData + expected_data.move_forward() + print('=' * 80) + print('We changed phase for admin game, moving from phase', expected_data.phase_index, + 'to phase', (expected_data.phase_index + 1), '/', len(expected_data.phases)) + print('=' * 80) + + # state_history must not be empty. + assert len(admin_game.state_history) == expected_data.phase_index, ( + len(admin_game.state_history), expected_data.phase_index) + + # Verify previous game state. + if admin_game.state_history: + expected_state = expected_data.phases[expected_data.phase_index - 1].state + expected_engine = Game(initial_state=expected_state) + given_state = admin_game.state_history.last_value() + given_engine = Game(initial_state=given_state) + + print('Verifying expected previous phase', expected_engine.get_current_phase()) + print('Verifying game processing from previous phase to next phase.') + + other_expected_engine = Game(initial_state=expected_state) + other_expected_engine.process() + other_given_engine = Game(initial_state=given_state) + other_given_engine.rules.append('SOLITAIRE') + other_given_engine.process() + assert other_expected_engine.get_current_phase() == other_given_engine.get_current_phase(), ( + 'Computed expected next phase %s, got computed given next phase %s' + % (other_expected_engine.get_current_phase(), other_given_engine.get_current_phase()) + ) + + assert expected_engine.map_name == given_engine.map_name + assert expected_engine.get_current_phase() == given_engine.get_current_phase() + + expected_orders = expected_engine.get_orders() + given_orders = given_engine.get_orders() + assert len(expected_orders) == len(given_orders), (expected_orders, given_orders) + for power_name in given_orders: + assert power_name in expected_orders, power_name + given_power_orders = list(sorted(given_orders[power_name])) + expected_power_orders = list(sorted(expected_orders[power_name])) + assert expected_power_orders == given_power_orders, ( + 'Power orders for %s\nExpected: %s\nGiven: %s\nAll given: %s\n' + % (power_name, expected_power_orders, given_power_orders, given_orders)) + + expected_units = expected_engine.get_units() + given_units = expected_engine.get_units() + assert len(expected_units) == len(given_units) + for power_name in given_units: + assert power_name in expected_units, (power_name, expected_units, given_units) + expected_power_units = list(sorted(expected_units[power_name])) + given_power_units = list(sorted(given_units[power_name])) + assert expected_power_units == given_power_units, ( + power_name, expected_power_units, given_power_units, given_units) + + expected_centers = expected_engine.get_centers() + given_centers = given_engine.get_centers() + assert len(expected_centers) == len(given_centers), (expected_centers, given_centers) + for power_name in given_centers: + assert power_name in expected_centers + expected_power_centers = list(sorted(expected_centers[power_name])) + given_power_centers = list(sorted(given_centers[power_name])) + assert expected_power_centers == given_power_centers, ( + power_name, expected_power_centers, given_power_centers) + + assert expected_engine.get_hash() == given_engine.get_hash(), ( + expected_engine.get_hash(), given_engine.get_hash()) + + if expected_data.phase_index >= len(expected_data.phases): + assert expected_data.phase_index == len(expected_data.phases) + assert admin_game.state_history.last_value()['name'] == expected_data.phases[-1].name, ( + 'Wrong last phase, expected %s, got %s' + % (admin_game.state_history.last_value()['name'], expected_data.phases[-1].name) + ) + print('Admin game terminated.') + +def on_admin_game_state_update(admin_game, notification): + """ Admin game notification callback for game state update. + :param admin_game: admin game + :param notification: notification + :type admin_game: NetworkGame + :type notification: diplomacy.communication.notifications.GamePhaseUpdate + """ + if notification.phase_data_type == strings.PHASE: + on_admin_game_phase_update(admin_game, None) + +def on_admin_powers_controllers(admin_game, notification): + """ Admin game notification callback for powers controllers received (unexpected). + :param admin_game: game + :param notification: notification + :type admin_game: NetworkGame + :type notification: diplomacy.communication.notifications.PowersControllers + """ + LOGGER.warning('%d dummy power(s).', + len([controller for controller in notification.powers.values() if controller == strings.DUMMY])) + +def on_admin_game_status_update(admin_game, notification): + """ Admin game notification callback for game status update. + :param admin_game: admin game + :param notification: notification + :type admin_game: NetworkGame + """ + print('(admin game) game status of %s updated to %s' % (admin_game.role, admin_game.status)) + +@gen.coroutine +def play_phase(game, expected_messages): + """ Play a phase for a user game: + 1) Send messages + 2) wait for messages to receive + 3) send current orders. + :param game: user game + :param expected_messages: expected messages + :type game: NetworkGame + :type expected_messages: ExpectedMessages + """ + while expected_messages.has_messages_to_send(): + yield gen.sleep(10e-6) + yield send_messages_if_needed(game, expected_messages) + while expected_messages.has_messages_to_receive(): + yield gen.sleep(10e-6) + yield send_current_orders(game) + +@gen.coroutine +def on_game_status_update(game, notification): + """ User game notification callback for game status update. + Used to start the game locally when game started on server. + :param game: game + :param notification: notification + :type game: NetworkGame + :type notification: diplomacy.communication.notifications.GameStatusUpdate + """ + LOGGER.warning('Game status of %s updated to %s', game.role, game.status) + expected_data = game.data # type: ExpectedData + if not expected_data.playing and game.is_game_active: + # Game started on server. + expected_data.playing = True + print('Playing.') + yield play_phase(game, expected_data.messages) + +@gen.coroutine +def verify_current_phase(game): + """ Check and play current phase. + :param game: a NetWork game object. + :type game: NetworkGame + """ + expected_data = game.data # type: ExpectedData + + # Verify current phase. + expected_messages = expected_data.messages + print('=' * 80) + print('Checking expected phase', expected_data.expected_phase.name, + '(%d/%d) for' % (expected_data.phase_index + 1, len(expected_data.phases)), expected_data.power_name, + 'with', len(expected_data.messages.messages), 'messages.') + print('=' * 80) + # Verify phase name. + if game.current_short_phase != str(expected_data.expected_phase.name): + raise AssertionError(str(expected_data.expected_phase.name), str(game.current_short_phase)) + + if game.is_game_active: + yield play_phase(game, expected_messages) + +def get_user_game_fn(case_data, power_name): + """ Return a coroutine procedure that loads and play a user game for given power name. + :param case_data: case data + :param power_name: str + :return: a procedure. + :type case_data: CaseData + """ + + @gen.coroutine + def load_fn(): + """ Coroutine for loading power game for given power name. """ + yield load_power_game(case_data, power_name) + + return load_fn + +def get_future_game_done_fn(power_name): + """ Return a callback to call when a power game is finished. + Callback currently just prints a message to tell that power game is terminated. + :param power_name: power name of associated game. + :return: a callable that receives the future done when game is finished. + """ + + def game_done_fn(future): + """ Function called when related game is done. """ + print('Game ended (%s).' % power_name) + + return game_done_fn + +@gen.coroutine +def load_power_game(case_data, power_name): + """ Load and play a power game from admin game for given power name. + :type case_data: CaseData + """ + print('Loading game for power', power_name) + + username = 'user_%s' % power_name + password = 'password_%s' % power_name + try: + user_channel = yield case_data.connection.authenticate(username, password, create_user=True) + except exceptions.ResponseException: + print('User', username, 'seems to already exists. Try to login.') + user_channel = yield case_data.connection.authenticate(username, password, create_user=False) + print('User', username, 'connected.') + + user_game = yield user_channel.join_game(game_id=case_data.admin_game.game_id, power_name=power_name) + assert user_game.is_player_game() + assert user_game.power.name == power_name + + case_data.user_games[power_name] = user_game + print('Game created for user %s.' % username, len(user_game.messages), len(user_game.state_history)) + + # Set notification callback for user game to manage messages received. + user_game.add_on_game_status_update(on_game_status_update) + user_game.add_on_game_message_received(on_message_received) + user_game.add_on_game_processed(case_data.on_power_phase_update) + user_game.add_on_game_phase_update(case_data.on_power_state_update) + + # Save expected data into attribute user_game.data. + user_game.data = ExpectedData(power_name=power_name, phases=case_data.phases, phase_index=0) + # Start to play and test game. + yield verify_current_phase(user_game) + +@gen.coroutine +def main(case_data): + """ Test real game environment with one game and all power controlled (no dummy powers). + This method may be called form a non-test code to run a real game case. + :param case_data: test data + :type case_data: CaseData + """ + # ================ + # Initialize test. + # ================ + if case_data.admin_channel is None: + LOGGER.info('Creating connection, admin channel and admin game.') + case_data.connection = yield connect(case_data.hostname, case_data.port) + case_data.admin_channel = yield case_data.connection.authenticate('admin', 'password', create_user=False) + # NB: For all test cases, first game state should be default game engine state when starting. + # So, we don't need to pass game state of first expected phase when creating a server game. + case_data.admin_game = yield case_data.admin_channel.create_game( + map_name=case_data.map_name, rules=case_data.rules, deadline=0) + assert case_data.admin_game.power_choice + assert case_data.admin_game.real_time + case_data.admin_game.data = ExpectedData(power_name='', phases=case_data.phases, phase_index=0) + case_data.admin_game.add_on_game_status_update(on_admin_game_status_update) + case_data.admin_game.add_on_game_processed(on_admin_game_phase_update) + case_data.admin_game.add_on_game_phase_update(on_admin_game_state_update) + case_data.admin_game.add_on_powers_controllers(on_admin_powers_controllers) + + # ========== + # Test game. + # ========== + + # Get available maps to retrieve map power names. + available_maps = yield case_data.admin_channel.get_available_maps() + print('Map: %s, powers:' % case_data.map_name, + ', '.join(power_name for power_name in sorted(available_maps[case_data.map_name]))) + # Load one game per power name. + for power_name in available_maps[case_data.map_name]['powers']: + case_data.future_games_ended[power_name] = Future() + case_data.future_games_ended[power_name].add_done_callback(get_future_game_done_fn(power_name)) + case_data.io_loop.add_callback(get_user_game_fn(case_data, power_name)) + + # Wait to let power games play. + print('Running ...') + yield case_data.future_games_ended + print('All game terminated. Just wait a little ...') + yield gen.sleep(2) + print('End running.') + +def run(case_data, **server_kwargs): + """ Real test function called for a given case data. + Load a server (with optional given server kwargs), + call function main(case_data) as client code + and wait for main function to terminate. + :type case_data: CaseData + """ + + print() + io_loop = IOLoop() + io_loop.make_current() + common.Tornado.stop_loop_on_callback_error(io_loop) + case_data.io_loop = io_loop + case_data.test_server = Server(**server_kwargs) + + @gen.coroutine + def coroutine_func(): + """ Concrete call to main function. """ + yield main(case_data) + case_data.io_loop.stop() + print('Finished', case_data.case_name, 'at', common.timestamp_microseconds()) + + io_loop.add_callback(coroutine_func) + case_data.test_server.start(case_data.port, io_loop) + case_data.io_loop.clear_current() + case_data.io_loop.close() + case_data.test_server.backend.http_server.stop() + +def test_maps(): + """ Building required maps to avoid timeout on the primary test """ + for map_name in ('ancmed', 'colonial', 'empire', 'known_world_901', 'modern', 'standard', + 'standard_france_austria', 'standard_germany_italy', 'world'): + Map(map_name) + +def test_3(): + """ Test case 3. """ + case_data = CaseData('3.json') + run(case_data, ping_seconds=constants.DEFAULT_PING_SECONDS) + # We must clear server caches to allow to re-create a Server with same test case but different server attributes. + Server.__cache__.clear() + +def test_3_ping_1s(): + """ Test case 3 with small ping (1 second). """ + case_data = CaseData('3.json') + run(case_data, ping_seconds=1) + # We must clear server caches to allow to re-create a Server with same test case but different server attributes. + Server.__cache__.clear() diff --git a/diplomacy/tests/test_datc.py b/diplomacy/tests/test_datc.py new file mode 100644 index 0000000..aca8079 --- /dev/null +++ b/diplomacy/tests/test_datc.py @@ -0,0 +1,5395 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" DATC Test Cases + - Contains the diplomacy adjudication test cases +""" +# pylint: disable=too-many-lines +from diplomacy.engine.game import Game + +# ----------------- +# DATC TEST CASES +# ----------------- +class TestDATC(): + """ DATC test cases""" + # pylint: disable=too-many-public-methods + + @staticmethod + def create_game(): + """ Creates a game object""" + return Game() + + @staticmethod + def clear_units(game): + """ Clears units """ + game.clear_units() + + @staticmethod + def clear_centers(game): + """ Clears supply centers """ + game.clear_centers() + + @staticmethod + def set_units(game, power_name, units): + """ Sets units on the map """ + game.set_units(power_name, units) + + @staticmethod + def set_centers(game, power_name, centers): + """ Transfers SC ownership to power """ + game.set_centers(power_name, centers) + + @staticmethod + def set_orders(game, power_name, orders): + """ Submit orders """ + game.set_orders(power_name, orders) + + @staticmethod + def process(game): + """ Processes the game """ + # Calculating hash before + hash_before_1 = game.get_hash() + hash_before_2 = game.rebuild_hash() + + # Processing + game.process() + + # Calculating hash after + hash_after_1 = game.get_hash() + hash_after_2 = game.rebuild_hash() + + # Checking + assert hash_before_1 == hash_before_2 + assert hash_after_1 == hash_after_2 + + @staticmethod + def owner_name(game, unit): + """ Retrieves owner name """ + has_coast = '/' in unit + owner = game._unit_owner(unit, coast_required=has_coast) # pylint: disable=protected-access + if owner is not None: + return owner.name + return None + + @staticmethod + def check_results(game, unit, value, phase='M'): + """ Checks adjudication results """ + # pylint: disable=too-many-return-statements + if not game: + return False + + result = game.result_history.last_value() + + # Checking if the results contain duplicate values + unit_result = result.get(unit, []) + if len(unit_result) != len(set(unit_result)): + raise RuntimeError('Duplicate values detected in %s' % unit_result) + + # Done self.processing a retreats phase + if phase == 'R': + if value == 'void' and 'void' in unit_result: + return True + if value == '': + success = unit not in game.popped and unit_result == [] + if not success: + print('Results: %s - Expected: []' % result.get(unit, '<Not Found>')) + return success + + success = unit in game.popped and value in unit_result + if not success: + print('Results: %s - Expected: %s' % (result.get(unit, '<Not Found>'), value)) + return success + + # Done self.processing a retreats phase + if phase == 'A': + if value == 'void' and 'void' in unit_result: + return True + success = value == unit_result + if not success: + print('Results: %s - Expected: %s' % (result.get(unit, '<Not Found>'), value)) + return success + + order_status = game.get_order_status(unit=unit) + + # Finding all ordered units + ordered_units = [] + for power_name in game.ordered_units: + ordered_units += game.ordered_units[power_name] + + # Invalid order + if value == 'void': + if 'void' in result.get(unit, []): + return True + if unit not in ordered_units: + return True + return False + + # Invalid unit + if unit not in game.command: + print('Results: %s NOT FOUND - Expected: %s' % (unit, value)) + return False + + # Expected no errors + if value == '': + if order_status: + print('Results: %s - Expected: []' % order_status) + return False + return True + + # Incorrect error + if value not in game.get_order_status(unit=unit): + print('Results: %s - Expected: %s' % (order_status, value)) + return False + + # Correct value + return True + + @staticmethod + def move_to_phase(game, new_phase): + """ Move to a specific phase""" + if not game: + raise RuntimeError() + game.set_current_phase(new_phase) + + # ------------- Tests -------------------- + def test_6_a_1(self): + """ 6.A.1 TEST CASE, MOVING TO AN AREA THAT IS NOT A NEIGHBOUR + Check if an illegal move (without convoy) will fail. + England: F North Sea - Picardy + Order should fail. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', 'F NTH') + self.set_orders(game, 'ENGLAND', 'F NTH - PIC') + self.process(game) + assert self.check_results(game, 'F NTH', 'void') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'F PIC') is None + + def test_6_a_2(self): + """ 6.A.2. TEST CASE, MOVE ARMY TO SEA + Check if an army could not be moved to open sea. + England: A Liverpool - Irish Sea + Order should fail. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', 'A LVP') + self.set_orders(game, 'ENGLAND', 'A LVP - IRI') + self.process(game) + assert self.check_results(game, 'A LVP', 'void') + assert self.owner_name(game, 'A LVP') == 'ENGLAND' + assert self.owner_name(game, 'A IRI') is None + + def test_6_a_3(self): + """ 6.A.3. TEST CASE, MOVE FLEET TO LAND + Check whether a fleet can not move to land. + Germany: F Kiel - Munich + Order should fail. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', 'F KIE') + self.set_orders(game, 'GERMANY', 'F KIE - MUN') + self.process(game) + assert self.check_results(game, 'F KIE', 'void') + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'F MUN') is None + + def test_6_a_4(self): + """ 6.A.4. TEST CASE, MOVE TO OWN SECTOR + Moving to the same sector is an illegal move (2000 rulebook, page 4, + "An Army can be ordered to move into an adjacent inland or coastal province."). + Germany: F Kiel - Kiel + Program should not crash. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', 'F KIE') + self.set_orders(game, 'GERMANY', 'F KIE - KIE') + self.process(game) + assert self.check_results(game, 'F KIE', 'void') + assert self.owner_name(game, 'F KIE') == 'GERMANY' + + def test_6_a_5(self): + """ 6.A.5. TEST CASE, MOVE TO OWN SECTOR WITH CONVOY + Moving to the same sector is still illegal with convoy (2000 rulebook, page 4, + "Note: An Army can move across water provinces from one coastal province to another..."). + England: F North Sea Convoys A Yorkshire - Yorkshire + England: A Yorkshire - Yorkshire + England: A Liverpool Supports A Yorkshire - Yorkshire + Germany: F London - Yorkshire + Germany: A Wales Supports F London - Yorkshire + The move of the army in Yorkshire is illegal. This makes the support of Liverpool also illegal and without + the support, the Germans have a stronger force. The army in London dislodges the army in Yorkshire. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A YOR', 'A LVP']) + self.set_units(game, 'GERMANY', ['F LON', 'A WAL']) + self.set_orders(game, 'ENGLAND', ['F LON C A YOR - YOR', 'A YOR - YOR', 'A LVP S A YOR - YOR']) + self.set_orders(game, 'GERMANY', ['F LON - YOR', 'A WAL S F LON - YOR']) + self.process(game) + assert self.check_results(game, 'A YOR', 'void') + assert self.check_results(game, 'A YOR', 'dislodged') + assert self.check_results(game, 'A LVP', 'void') + assert check_dislodged(game, 'A YOR', 'F LON') + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'A WAL', '') + assert self.owner_name(game, 'F YOR') == 'GERMANY' + + def test_6_a_6(self): + """ 6.A.6. TEST CASE, ORDERING A UNIT OF ANOTHER COUNTRY + Check whether someone can not order a unit that is not his own unit. + England has a fleet in London. + Germany: F London - North Sea + Order should fail. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F LON']) + self.set_orders(game, 'GERMANY', ['F LON - NTH']) + self.process(game) + assert self.check_results(game, 'F LON', '') + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'F NTH') is None + + def test_6_a_7(self): + """ 6.A.7. TEST CASE, ONLY ARMIES CAN BE CONVOYED + A fleet can not be convoyed. + England: F London - Belgium + England: F North Sea Convoys A London - Belgium + Move from London to Belgium should fail. + """ + # ------------------------------------------- + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F LON', 'F NTH']) + self.set_orders(game, 'ENGLAND', ['F LON - BEL', 'F NTH C A LON - BEL']) + self.process(game) + assert self.check_results(game, 'F LON', 'void') + assert self.check_results(game, 'F NTH', 'void') + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'F BEL') is None + # ------------------------------------------- + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F LON', 'F NTH']) + self.set_orders(game, 'ENGLAND', ['F LON - BEL', 'F NTH C LON - BEL']) + self.process(game) + assert self.check_results(game, 'F LON', 'void') + assert self.check_results(game, 'F NTH', 'void') + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'F BEL') is None + # ------------------------------------------- + + def test_6_a_8(self): + """ 6.A.8. TEST CASE, SUPPORT TO HOLD YOURSELF IS NOT POSSIBLE + An army can not get an additional hold power by supporting itself. + Italy: A Venice - Trieste + Italy: A Tyrolia Supports A Venice - Trieste + Austria: F Trieste Supports F Trieste + The army in Trieste should be dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ITALY', ['A VEN', 'A TYR']) + self.set_units(game, 'AUSTRIA', 'F TRI') + self.set_orders(game, 'ITALY', ['A VEN - TRI', 'A TYR S A VEN - TRI']) + self.set_orders(game, 'AUSTRIA', 'F TRI S F TRI') + self.process(game) + assert self.check_results(game, 'F TRI', 'void') + assert self.check_results(game, 'F TRI', 'dislodged') + assert check_dislodged(game, 'F TRI', 'A VEN') + assert self.owner_name(game, 'A TRI') == 'ITALY' + assert self.owner_name(game, 'A VEN') is None + + def test_6_a_9(self): + """ 6.A.9. TEST CASE, FLEETS MUST FOLLOW COAST IF NOT ON SEA + If two places are adjacent, that does not mean that a fleet can move between + those two places. An implementation that only holds one list of adj. places for each place, is incorrect + Italy: F Rome - Venice + Move fails. An army can go from Rome to Venice, but a fleet can not. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ITALY', 'F ROM') + self.set_orders(game, 'ITALY', 'F ROM - VEN') + self.process(game) + assert self.check_results(game, 'F ROM', 'void') + assert self.owner_name(game, 'F ROM') == 'ITALY' + assert self.owner_name(game, 'F VEN') is None + + def test_6_a_10(self): + """ 6.A.10. TEST CASE, SUPPORT ON UNREACHABLE DESTINATION NOT POSSIBLE + The destination of the move that is supported must be reachable by the supporting unit. + Austria: A Venice Hold + Italy: F Rome Supports A Apulia - Venice + Italy: A Apulia - Venice + The support of Rome is illegal, because Venice can not be reached from Rome by a fleet. + Venice is not dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ITALY', ['F ROM', 'A APU']) + self.set_units(game, 'AUSTRIA', 'A VEN') + self.set_orders(game, 'ITALY', ['F ROM S A APU - VEN', 'A APU - VEN']) + self.set_orders(game, 'AUSTRIA', 'A VEN H') + self.process(game) + assert self.check_results(game, 'F ROM', 'void') + assert self.check_results(game, 'A APU', 'bounce') + assert self.owner_name(game, 'F ROM') == 'ITALY' + assert self.owner_name(game, 'A VEN') == 'AUSTRIA' + + def test_6_a_11(self): + """ 6.A.11. TEST CASE, SIMPLE BOUNCE + Two armies bouncing on each other. + Austria: A Vienna - Tyrolia + Italy: A Venice - Tyrolia + The two units bounce. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ITALY', 'A VEN') + self.set_units(game, 'AUSTRIA', 'A VIE') + self.set_orders(game, 'ITALY', 'A VEN - TYR') + self.set_orders(game, 'AUSTRIA', 'A VIE - TYR') + self.process(game) + assert self.check_results(game, 'A VEN', 'bounce') + assert self.check_results(game, 'A VIE', 'bounce') + assert self.owner_name(game, 'A VEN') == 'ITALY' + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A TYR') is None + + def test_6_a_12(self): + """ 6.A.12. TEST CASE, BOUNCE OF THREE UNITS + If three units move to the same place, the adjudicator should not bounce + the first two units and then let the third unit go to the now open place. + Austria: A Vienna - Tyrolia + Germany: A Munich - Tyrolia + Italy: A Venice - Tyrolia + The three units bounce. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', 'A VIE') + self.set_units(game, 'GERMANY', 'A MUN') + self.set_units(game, 'ITALY', 'A VEN') + self.set_orders(game, 'AUSTRIA', 'A VIE - TYR') + self.set_orders(game, 'GERMANY', 'A MUN - TYR') + self.set_orders(game, 'ITALY', 'A VEN - TYR') + self.process(game) + assert self.check_results(game, 'A VEN', 'bounce') + assert self.check_results(game, 'A VIE', 'bounce') + assert self.check_results(game, 'A MUN', 'bounce') + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'A VEN') == 'ITALY' + assert self.owner_name(game, 'A TYR') is None + + # 6.B. TEST CASES, COASTAL ISSUES + def test_6_b_1(self): + """ 6.B.1. TEST CASE, MOVING WITH UNSPECIFIED COAST WHEN COAST IS NECESSARY + Coast is significant in this case: + France: F Portugal - Spain + Some adjudicators take a default coast (see issue 4.B.1). + I prefer that the move fails. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', 'F POR') + self.set_orders(game, 'FRANCE', 'F POR - SPA') + self.process(game) + assert self.check_results(game, 'F POR', 'void') + assert self.owner_name(game, 'F POR') == 'FRANCE' + assert self.owner_name(game, 'F SPA') is None + assert self.owner_name(game, 'F SPA/NC') is None + assert self.owner_name(game, 'F SPA/SC') is None + + def test_6_b_2(self): + """ 6.B.2. TEST CASE, MOVING WITH UNSPECIFIED COAST WHEN COAST IS NOT NECESSARY + There is only one coast possible in this case: + France: F Gascony - Spain + Since the North Coast is the only coast that can be reached, it seems logical that + the a move is attempted to the north coast of Spain. Some adjudicators require that a coast + is also specified in this case and will decide that the move fails or take a default coast (see 4.B.2). + I prefer that an attempt is made to the only possible coast, the north coast of Spain. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', 'F GAS') + self.set_orders(game, 'FRANCE', 'F GAS - SPA') + self.process(game) + assert self.check_results(game, 'F GAS', '') + assert self.owner_name(game, 'F GAS') is None + assert self.owner_name(game, 'F SPA/NC') == 'FRANCE' + assert self.owner_name(game, 'F SPA/SC') is None + + def test_6_b_3(self): + """ 6.B.3. TEST CASE, MOVING WITH WRONG COAST WHEN COAST IS NOT NECESSARY + If only one coast is possible, but the wrong coast can be specified. + France: F Gascony - Spain(sc) + If the rules are played very clemently, a move will be attempted to the north coast of Spain. + However, since this order is very clear and precise, it is more common that the move fails (see 4.B.3). + I prefer that the move fails. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', 'F GAS') + self.set_orders(game, 'FRANCE', 'F GAS - SPA/SC') + self.process(game) + assert self.check_results(game, 'F GAS', 'void') + assert self.owner_name(game, 'F GAS') == 'FRANCE' + assert self.owner_name(game, 'F SPA') is None + assert self.owner_name(game, 'F SPA/NC') is None + assert self.owner_name(game, 'F SPA/SC') is None + + def test_6_b_4(self): + """ 6.B.4. TEST CASE, SUPPORT TO UNREACHABLE COAST ALLOWED + A fleet can give support to a coast where it can not go. + France: F Gascony - Spain(nc) + France: F Marseilles Supports F Gascony - Spain(nc) + Italy: F Western Mediterranean - Spain(sc) + Although the fleet in Marseilles can not go to the north coast it can still + support targeting the north coast. So, the support is successful, the move of the fleet + in Gasgony succeeds and the move of the Italian fleet fails. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['F GAS', 'F MAR']) + self.set_units(game, 'ITALY', 'F WES') + self.set_orders(game, 'FRANCE', ['F GAS - SPA/NC', 'F MAR S F GAS - SPA/NC']) + self.set_orders(game, 'ITALY', 'F WES - SPA/SC') + self.process(game) + assert self.check_results(game, 'F WES', 'bounce') + assert self.owner_name(game, 'F SPA/NC') == 'FRANCE' + assert self.owner_name(game, 'F MAR') == 'FRANCE' + assert self.owner_name(game, 'F WES') == 'ITALY' + + def test_6_b_5(self): + """ 6.B.5. TEST CASE, SUPPORT FROM UNREACHABLE COAST NOT ALLOWED + A fleet can not give support to an area that can not be reached from the current coast of the fleet. + France: F Marseilles - Gulf of Lyon + France: F Spain(nc) Supports F Marseilles - Gulf of Lyon + Italy: F Gulf of Lyon Hold + The Gulf of Lyon can not be reached from the North Coast of Spain. Therefore, the support of + Spain is invalid and the fleet in the Gulf of Lyon is not dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['F MAR', 'F SPA/NC']) + self.set_units(game, 'ITALY', 'F LYO') + self.set_orders(game, 'FRANCE', ['F MAR - LYO', 'F SPA/NC S F MAR - LYO']) + self.set_orders(game, 'ITALY', 'F LYO H') + self.process(game) + assert self.check_results(game, 'F SPA/NC', 'void') + assert self.check_results(game, 'F MAR', 'bounce') + assert self.owner_name(game, 'F MAR') == 'FRANCE' + assert self.owner_name(game, 'F SPA/NC') == 'FRANCE' + assert self.owner_name(game, 'F LYO') == 'ITALY' + + def test_6_b_6(self): + """ 6.B.6. TEST CASE, SUPPORT CAN BE CUT WITH OTHER COAST + Support can be cut from the other coast. + England: F Irish Sea Supports F North Atlantic Ocean - Mid-Atlantic Ocean + England: F North Atlantic Ocean - Mid-Atlantic Ocean + France: F Spain(nc) Supports F Mid-Atlantic Ocean + France: F Mid-Atlantic Ocean Hold + Italy: F Gulf of Lyon - Spain(sc) + The Italian fleet in the Gulf of Lyon will cut the support in Spain. That means + that the French fleet in the Mid Atlantic Ocean will be dislodged by the English fleet + in the North Atlantic Ocean. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F IRI', 'F NAO']) + self.set_units(game, 'FRANCE', ['F SPA/NC', 'F MAO']) + self.set_units(game, 'ITALY', 'F LYO') + self.set_orders(game, 'ENGLAND', ['F IRI S F NAO - MAO', 'F NAO - MAO']) + self.set_orders(game, 'FRANCE', ['F SPA/NC S F MAO', 'F MAO H']) + self.set_orders(game, 'ITALY', 'F LYO - SPA/SC') + self.process(game) + assert self.check_results(game, 'F SPA/NC', 'cut') + assert self.check_results(game, 'F MAO', 'dislodged') + assert check_dislodged(game, 'F MAO', 'F NAO') + assert self.owner_name(game, 'F IRI') == 'ENGLAND' + assert self.owner_name(game, 'F NAO') is None + assert self.owner_name(game, 'F MAO') == 'ENGLAND' + assert self.owner_name(game, 'F SPA/NC') == 'FRANCE' + assert self.owner_name(game, 'F LYO') == 'ITALY' + + def test_6_b_7(self): + """ 6.B.7. TEST CASE, SUPPORTING WITH UNSPECIFIED COAST + Most house rules accept support orders without coast specification. + France: F Portugal Supports F Mid-Atlantic Ocean - Spain + France: F Mid-Atlantic Ocean - Spain(nc) + Italy: F Gulf of Lyon Supports F Western Mediterranean - Spain(sc) + Italy: F Western Mediterranean - Spain(sc) + See issue 4.B.4. If coasts are not required in support orders, then the support of Portugal is successful. + This means that the Italian fleet in the Western Mediterranean bounces. Some adjudicators may not accept a + support order without coast (the support will fail or a default coast is taken). In that case the + support order of Portugal fails (in case of a default coast the coast will probably the south coast) and + the Italian fleet in the Western Mediterranean will successfully move. + I prefer that the support succeeds and the Italian fleet in the Western Mediterranean bounces. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['F POR', 'F MAO']) + self.set_units(game, 'ITALY', ['F LYO', 'F WES']) + self.set_orders(game, 'FRANCE', ['F POR S F MAO - SPA', 'F MAO - SPA/NC']) + self.set_orders(game, 'ITALY', ['F LYO S F WES - SPA/SC', 'F WES - SPA/SC']) + self.process(game) + assert self.check_results(game, 'F POR', '') + assert self.check_results(game, 'F LYO', '') + assert self.check_results(game, 'F MAO', 'bounce') + assert self.check_results(game, 'F WES', 'bounce') + assert self.owner_name(game, 'F POR') == 'FRANCE' + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'F LYO') == 'ITALY' + assert self.owner_name(game, 'F WES') == 'ITALY' + assert self.owner_name(game, 'F SPA') is None + assert self.owner_name(game, 'F SPA/SC') is None + assert self.owner_name(game, 'F SPA/NC') is None + + def test_6_b_8(self): + """ 6.B.8. TEST CASE, SUPPORTING WITH UNSPECIFIED COAST WHEN ONLY ONE COAST IS POSSIBLE + Some hardliners require a coast in a support order even when only one coast is possible. + France: F Portugal Supports F Gascony - Spain + France: F Gascony - Spain(nc) + Italy: F Gulf of Lyon Supports F Western Mediterranean - Spain(sc) + Italy: F Western Mediterranean - Spain(sc) + See issue 4.B.4. If coasts are not required in support orders, then the support of Portugal is successful. + This means that the Italian fleet in the Western Mediterranean bounces. Some adjudicators may not accept a + support order without coast (the support will fail or a default coast is taken). In that case the + support order of Portugal fails (in case of a default coast the coast will probably the south coast) and + the Italian fleet in the Western Mediterranean will successfully move. + I prefer that supporting without coasts should be allowed. So I prefer that the support of Portugal + is successful and that the Italian fleet in the Western Mediterranean bounces. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['F POR', 'F GAS']) + self.set_units(game, 'ITALY', ['F LYO', 'F WES']) + self.set_orders(game, 'FRANCE', ['F POR S F GAS - SPA', 'F GAS - SPA/NC']) + self.set_orders(game, 'ITALY', ['F LYO S F WES - SPA/SC', 'F WES - SPA/SC']) + self.process(game) + assert self.check_results(game, 'F POR', '') + assert self.check_results(game, 'F LYO', '') + assert self.check_results(game, 'F GAS', 'bounce') + assert self.check_results(game, 'F WES', 'bounce') + assert self.owner_name(game, 'F POR') == 'FRANCE' + assert self.owner_name(game, 'F GAS') == 'FRANCE' + assert self.owner_name(game, 'F LYO') == 'ITALY' + assert self.owner_name(game, 'F WES') == 'ITALY' + assert self.owner_name(game, 'F SPA') is None + assert self.owner_name(game, 'F SPA/SC') is None + assert self.owner_name(game, 'F SPA/NC') is None + + def test_6_b_9(self): + """ 6.B.9. TEST CASE, SUPPORTING WITH WRONG COAST + Coasts can be specified in a support, but the result depends on the house rules. + France: F Portugal Supports F Mid-Atlantic Ocean - Spain(nc) + France: F Mid-Atlantic Ocean - Spain(sc) + Italy: F Gulf of Lyon Supports F Western Mediterranean - Spain(sc) + Italy: F Western Mediterranean - Spain(sc) + See issue 4.B.4. If it is required that the coast matches, then the support of the French fleet in the + Mid-Atlantic Ocean fails and that the Italian fleet in the Western Mediterranean moves successfully. Some + adjudicators ignores the coasts in support orders. In that case, the move of the Italian fleet bounces. + I prefer that the support fails and that the Italian fleet in the Western Mediterranean moves successfully. + """ + # Order expansion will rewrite F POR S F MAO - SPA/NC -> F POR S F MAO - SPA/SC + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['F POR', 'F MAO']) + self.set_units(game, 'ITALY', ['F LYO', 'F WES']) + self.set_orders(game, 'FRANCE', ['F POR S F MAO - SPA/NC', 'F MAO - SPA/SC']) + self.set_orders(game, 'ITALY', ['F LYO S F WES - SPA/SC', 'F WES - SPA/SC']) + self.process(game) + assert self.check_results(game, 'F POR', '') + assert self.check_results(game, 'F MAO', 'bounce') + assert self.check_results(game, 'F LYO', '') + assert self.check_results(game, 'F WES', 'bounce') + assert self.owner_name(game, 'F POR') == 'FRANCE' + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'F SPA') is None + assert self.owner_name(game, 'F SPA/NC') is None + assert self.owner_name(game, 'F SPA/SC') is None + assert self.owner_name(game, 'F LYO') == 'ITALY' + assert self.owner_name(game, 'F WES') == 'ITALY' + + def test_6_b_10(self): + """ 6.B.10. TEST CASE, UNIT ORDERED WITH WRONG COAST + A player might specify the wrong coast for the ordered unit. + France has a fleet on the south coast of Spain and orders: + France: F Spain(nc) - Gulf of Lyon + If only perfect orders are accepted, then the move will fail, but since the coast for the ordered unit + has no purpose, it might also be ignored (see issue 4.B.5). + I prefer that a move will be attempted. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', 'F SPA/SC') + self.set_orders(game, 'FRANCE', 'F SPA/NC - LYO') + self.process(game) + assert self.check_results(game, 'F SPA/SC', '') + assert self.owner_name(game, 'F SPA') is None + assert self.owner_name(game, 'F SPA/SC') is None + assert self.owner_name(game, 'F LYO') == 'FRANCE' + + def test_6_b_11(self): + """ 6.B.11. TEST CASE, COAST CAN NOT BE ORDERED TO CHANGE + The coast can not change by just ordering the other coast. + France has a fleet on the north coast of Spain and orders: + France: F Spain(sc) - Gulf of Lyon + The move fails. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', 'F SPA/NC') + self.set_orders(game, 'FRANCE', 'F SPA/SC - LYO') + self.process(game) + assert self.check_results(game, 'F SPA/SC', 'void') + assert self.owner_name(game, 'F SPA') == 'FRANCE' + assert self.owner_name(game, 'F SPA/NC') == 'FRANCE' + assert self.owner_name(game, 'F SPA/SC') is None + assert self.owner_name(game, 'F LYO') is None + + def test_6_b_12(self): + """ 6.B.12. TEST CASE, ARMY MOVEMENT WITH COASTAL SPECIFICATION + For armies the coasts are irrelevant: + France: A Gascony - Spain(nc) + If only perfect orders are accepted, then the move will fail. But it is also possible that coasts are + ignored in this case and a move will be attempted (see issue 4.B.6). + I prefer that a move will be attempted. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', 'A GAS') + self.set_orders(game, 'FRANCE', 'A GAS - SPA/NC') + self.process(game) + assert self.check_results(game, 'A GAS', '') + assert self.owner_name(game, 'A GAS') is None + assert self.owner_name(game, 'A SPA') == 'FRANCE' + assert self.owner_name(game, 'A SPA/NC') is None + assert self.owner_name(game, 'A SPA/SC') is None + + def test_6_b_13(self): + """ 6.B.13. TEST CASE, COASTAL CRAWL NOT ALLOWED + If a fleet is leaving a sector from a certain coast while in the opposite direction another fleet + is moving to another coast of the sector, it is still a head to head battle. This has been decided in + the great revision of the 1961 rules that resulted in the 1971 rules. + Turkey: F Bulgaria(sc) - Constantinople + Turkey: F Constantinople - Bulgaria(ec) + Both moves fail. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'TURKEY', ['F BUL/SC', 'F CON']) + self.set_orders(game, 'TURKEY', ['F BUL/SC - CON', 'F CON - BUL/EC']) + self.process(game) + assert self.check_results(game, 'F BUL/SC', 'bounce') + assert self.check_results(game, 'F CON', 'bounce') + assert self.owner_name(game, 'F BUL/SC') == 'TURKEY' + assert self.owner_name(game, 'F CON') == 'TURKEY' + assert self.owner_name(game, 'F BUL/EC') is None + + def test_6_b_14(self): + """ 6.B.14. TEST CASE, BUILDING WITH UNSPECIFIED COAST + Coast must be specified in certain build cases: + Russia: Build F St Petersburg + If no default coast is taken (see issue 4.B.7), the build fails. + I do not like default coast, so I prefer that the build fails. + """ + game = self.create_game() + self.clear_units(game) + self.set_centers(game, 'RUSSIA', 'STP') + self.move_to_phase(game, 'W1901A') + self.set_orders(game, 'RUSSIA', 'F STP B') + self.process(game) + assert self.check_results(game, 'F STP', 'void', phase='A') + assert self.owner_name(game, 'F STP') is None + assert self.owner_name(game, 'F STP/SC') is None + assert self.owner_name(game, 'F STP/NC') is None + + # 6.C. TEST CASES, CIRCULAR MOVEMENT + def test_6_c_1(self): + """ 6.C.1. TEST CASE, THREE ARMY CIRCULAR MOVEMENT + Three units can change place, even in spring 1901. + Turkey: F Ankara - Constantinople + Turkey: A Constantinople - Smyrna + Turkey: A Smyrna - Ankara + All three units will move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'TURKEY', ['F ANK', 'A CON', 'A SMY']) + self.set_orders(game, 'TURKEY', ['F ANK - CON', 'A CON - SMY', 'A SMY - ANK']) + self.process(game) + assert self.check_results(game, 'F ANK', '') + assert self.check_results(game, 'A CON', '') + assert self.check_results(game, 'A SMY', '') + assert self.owner_name(game, 'A ANK') == 'TURKEY' + assert self.owner_name(game, 'F CON') == 'TURKEY' + assert self.owner_name(game, 'A SMY') == 'TURKEY' + + def test_6_c_2(self): + """ 6.C.2. TEST CASE, THREE ARMY CIRCULAR MOVEMENT WITH SUPPORT + Three units can change place, even when one gets support. + Turkey: F Ankara - Constantinople + Turkey: A Constantinople - Smyrna + Turkey: A Smyrna - Ankara + Turkey: A Bulgaria Supports F Ankara - Constantinople + Of course the three units will move, but knowing how programs are written, this can confuse the adjudicator. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'TURKEY', ['F ANK', 'A CON', 'A SMY', 'A BUL']) + self.set_orders(game, 'TURKEY', ['F ANK - CON', 'A CON - SMY', 'A SMY - ANK', 'A BUL S F ANK - CON']) + self.process(game) + assert self.check_results(game, 'F ANK', '') + assert self.check_results(game, 'A CON', '') + assert self.check_results(game, 'A SMY', '') + assert self.check_results(game, 'A BUL', '') + assert self.owner_name(game, 'A ANK') == 'TURKEY' + assert self.owner_name(game, 'F CON') == 'TURKEY' + assert self.owner_name(game, 'A SMY') == 'TURKEY' + assert self.owner_name(game, 'A BUL') == 'TURKEY' + + def test_6_c_3(self): + """ 6.C.3. TEST CASE, A DISRUPTED THREE ARMY CIRCULAR MOVEMENT + When one of the units bounces, the whole circular movement will hold. + Turkey: F Ankara - Constantinople + Turkey: A Constantinople - Smyrna + Turkey: A Smyrna - Ankara + Turkey: A Bulgaria - Constantinople + Every unit will keep its place. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'TURKEY', ['F ANK', 'A CON', 'A SMY', 'A BUL']) + self.set_orders(game, 'TURKEY', ['F ANK - CON', 'A CON - SMY', 'A SMY - ANK', 'A BUL - CON']) + self.process(game) + assert self.check_results(game, 'F ANK', 'bounce') + assert self.check_results(game, 'A CON', 'bounce') + assert self.check_results(game, 'A SMY', 'bounce') + assert self.check_results(game, 'A BUL', 'bounce') + assert self.owner_name(game, 'F ANK') == 'TURKEY' + assert self.owner_name(game, 'A CON') == 'TURKEY' + assert self.owner_name(game, 'A SMY') == 'TURKEY' + assert self.owner_name(game, 'A BUL') == 'TURKEY' + + def test_6_c_4(self): + """ 6.C.4. TEST CASE, A CIRCULAR MOVEMENT WITH ATTACKED CONVOY + When the circular movement contains an attacked convoy, the circular movement succeeds. + The adjudication algorithm should handle attack of convoys before calculating circular movement. + Austria: A Trieste - Serbia + Austria: A Serbia - Bulgaria + Turkey: A Bulgaria - Trieste + Turkey: F Aegean Sea Convoys A Bulgaria - Trieste + Turkey: F Ionian Sea Convoys A Bulgaria - Trieste + Turkey: F Adriatic Sea Convoys A Bulgaria - Trieste + Italy: F Naples - Ionian Sea + The fleet in the Ionian Sea is attacked but not dislodged. The circular movement succeeds. + The Austrian and Turkish armies will advance. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['A TRI', 'A SER']) + self.set_units(game, 'TURKEY', ['A BUL', 'F AEG', 'F ION', 'F ADR']) + self.set_units(game, 'ITALY', 'F NAP') + self.set_orders(game, 'AUSTRIA', ['A TRI - SER', 'A SER - BUL']) + self.set_orders(game, 'TURKEY', ['A BUL - TRI', 'F AEG C A BUL - TRI', 'F ION C A BUL - TRI', + 'F ADR C A BUL - TRI']) + self.set_orders(game, 'ITALY', 'F NAP - ION') + self.process(game) + assert self.check_results(game, 'A TRI', '') + assert self.check_results(game, 'A SER', '') + assert self.check_results(game, 'A BUL', '') + assert self.check_results(game, 'F AEG', '') + assert self.check_results(game, 'F ION', '') + assert self.check_results(game, 'F ADR', '') + assert self.check_results(game, 'F NAP', 'bounce') + assert self.owner_name(game, 'A TRI') == 'TURKEY' + assert self.owner_name(game, 'A SER') == 'AUSTRIA' + assert self.owner_name(game, 'A BUL') == 'AUSTRIA' + assert self.owner_name(game, 'F AEG') == 'TURKEY' + assert self.owner_name(game, 'F ION') == 'TURKEY' + assert self.owner_name(game, 'F ADR') == 'TURKEY' + assert self.owner_name(game, 'F NAP') == 'ITALY' + + def test_6_c_5(self): + """ 6.C.5. TEST CASE, A DISRUPTED CIRCULAR MOVEMENT DUE TO DISLODGED CONVOY + When the circular movement contains a convoy, the circular movement is disrupted when the convoying + fleet is dislodged. The adjudication algorithm should disrupt convoys before calculating circular movement. + Austria: A Trieste - Serbia + Austria: A Serbia - Bulgaria + Turkey: A Bulgaria - Trieste + Turkey: F Aegean Sea Convoys A Bulgaria - Trieste + Turkey: F Ionian Sea Convoys A Bulgaria - Trieste + Turkey: F Adriatic Sea Convoys A Bulgaria - Trieste + Italy: F Naples - Ionian Sea + Italy: F Tunis Supports F Naples - Ionian Sea + Due to the dislodged convoying fleet, all Austrian and Turkish armies will not move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['A TRI', 'A SER']) + self.set_units(game, 'TURKEY', ['A BUL', 'F AEG', 'F ION', 'F ADR']) + self.set_units(game, 'ITALY', ['F NAP', 'F TUN']) + self.set_orders(game, 'AUSTRIA', ['A TRI - SER', 'A SER - BUL']) + self.set_orders(game, 'TURKEY', ['A BUL - TRI', 'F AEG C A BUL - TRI', 'F ION C A BUL - TRI', + 'F ADR C A BUL - TRI']) + self.set_orders(game, 'ITALY', ['F NAP - ION', 'F TUN S F NAP - ION']) + self.process(game) + assert self.check_results(game, 'A TRI', 'bounce') + assert self.check_results(game, 'A SER', 'bounce') + assert self.check_results(game, 'A BUL', 'no convoy') + assert self.check_results(game, 'F AEG', 'no convoy') + assert self.check_results(game, 'F ION', 'dislodged') + assert self.check_results(game, 'F ADR', 'no convoy') + assert self.check_results(game, 'F NAP', '') + assert self.check_results(game, 'F TUN', '') + assert check_dislodged(game, 'F ION', 'F NAP') + assert self.owner_name(game, 'A TRI') == 'AUSTRIA' + assert self.owner_name(game, 'A SER') == 'AUSTRIA' + assert self.owner_name(game, 'A BUL') == 'TURKEY' + assert self.owner_name(game, 'F AEG') == 'TURKEY' + assert self.owner_name(game, 'F ION') == 'ITALY' + assert self.owner_name(game, 'F ADR') == 'TURKEY' + assert self.owner_name(game, 'F NAP') is None + assert self.owner_name(game, 'F TUN') == 'ITALY' + + def test_6_c_6(self): + """ 6.C.6. TEST CASE, TWO ARMIES WITH TWO CONVOYS + Two armies can swap places even when they are not adjacent. + England: F North Sea Convoys A London - Belgium + England: A London - Belgium + France: F English Channel Convoys A Belgium - London + France: A Belgium - London + Both convoys should succeed. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A LON']) + self.set_units(game, 'FRANCE', ['F ENG', 'A BEL']) + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - BEL', 'A LON - BEL']) + self.set_orders(game, 'FRANCE', ['F ENG C A BEL - LON', 'A BEL - LON']) + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'A BEL', '') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A BEL') == 'ENGLAND' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'A LON') == 'FRANCE' + + def test_6_c_7(self): + """ 6.C.7. TEST CASE, DISRUPTED UNIT SWAP + If in a swap one of the unit bounces, then the swap fails. + England: F North Sea Convoys A London - Belgium + England: A London - Belgium + France: F English Channel Convoys A Belgium - London + France: A Belgium - London + France: A Burgundy - Belgium + None of the units will succeed to move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A LON']) + self.set_units(game, 'FRANCE', ['F ENG', 'A BEL', 'A BUR']) + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - BEL', 'A LON - BEL']) + self.set_orders(game, 'FRANCE', ['F ENG C A BEL - LON', 'A BEL - LON', 'A BUR - BEL']) + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A LON', 'bounce') + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'A BEL', 'bounce') + assert self.check_results(game, 'A BUR', 'bounce') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A LON') == 'ENGLAND' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'A BEL') == 'FRANCE' + assert self.owner_name(game, 'A BUR') == 'FRANCE' + + # 6.D. TEST CASES, SUPPORTS AND DISLODGES + def test_6_d_1(self): + """ 6.D.1. TEST CASE, SUPPORTED HOLD CAN PREVENT DISLODGEMENT + The most simple support to hold order. + Austria: F Adriatic Sea Supports A Trieste - Venice + Austria: A Trieste - Venice + Italy: A Venice Hold + Italy: A Tyrolia Supports A Venice + The support of Tyrolia prevents that the army in Venice is dislodged. The army in Trieste will not move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['F ADR', 'A TRI']) + self.set_units(game, 'ITALY', ['A VEN', 'A TYR']) + self.set_orders(game, 'AUSTRIA', ['F ADR S A TRI - VEN', 'A TRI - VEN']) + self.set_orders(game, 'ITALY', ['A VEN H', 'A TYR S A VEN']) + self.process(game) + assert self.check_results(game, 'F ADR', '') + assert self.check_results(game, 'A TRI', 'bounce') + assert self.check_results(game, 'A VEN', '') + assert self.check_results(game, 'A TYR', '') + assert self.owner_name(game, 'F ADR') == 'AUSTRIA' + assert self.owner_name(game, 'A TRI') == 'AUSTRIA' + assert self.owner_name(game, 'A VEN') == 'ITALY' + assert self.owner_name(game, 'A TYR') == 'ITALY' + + def test_6_d_2(self): + """ 6.D.2. TEST CASE, A MOVE CUTS SUPPORT ON HOLD + The most simple support on hold cut. + Austria: F Adriatic Sea Supports A Trieste - Venice + Austria: A Trieste - Venice + Austria: A Vienna - Tyrolia + Italy: A Venice Hold + Italy: A Tyrolia Supports A Venice + The support of Tyrolia is cut by the army in Vienna. That means that the army in Venice is dislodged by the + army from Trieste. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['F ADR', 'A TRI', 'A VIE']) + self.set_units(game, 'ITALY', ['A VEN', 'A TYR']) + self.set_orders(game, 'AUSTRIA', ['F ADR S A TRI - VEN', 'A TRI - VEN', 'A VIE - TYR']) + self.set_orders(game, 'ITALY', ['A VEN H', 'A TYR S A VEN']) + self.process(game) + assert self.check_results(game, 'F ADR', '') + assert self.check_results(game, 'A TRI', '') + assert self.check_results(game, 'A VIE', 'bounce') + assert self.check_results(game, 'A VEN', 'dislodged') + assert self.check_results(game, 'A TYR', 'cut') + assert check_dislodged(game, 'A VEN', 'A TRI') + assert self.owner_name(game, 'F ADR') == 'AUSTRIA' + assert self.owner_name(game, 'A TRI') is None + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A VEN') == 'AUSTRIA' + assert self.owner_name(game, 'A TYR') == 'ITALY' + + def test_6_d_3(self): + """ 6.D.3. TEST CASE, A MOVE CUTS SUPPORT ON MOVE + The most simple support on move cut. + Austria: F Adriatic Sea Supports A Trieste - Venice + Austria: A Trieste - Venice + Italy: A Venice Hold + Italy: F Ionian Sea - Adriatic Sea + The support of the fleet in the Adriatic Sea is cut. That means that the army in Venice will not be + dislodged and the army in Trieste stays in Trieste. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['F ADR', 'A TRI']) + self.set_units(game, 'ITALY', ['A VEN', 'F ION']) + self.set_orders(game, 'AUSTRIA', ['F ADR S A TRI - VEN', 'A TRI - VEN']) + self.set_orders(game, 'ITALY', ['A VEN H', 'F ION - ADR']) + self.process(game) + assert self.check_results(game, 'F ADR', 'cut') + assert self.check_results(game, 'A TRI', 'bounce') + assert self.check_results(game, 'A VEN', '') + assert self.check_results(game, 'F ION', 'bounce') + assert self.owner_name(game, 'F ADR') == 'AUSTRIA' + assert self.owner_name(game, 'A TRI') == 'AUSTRIA' + assert self.owner_name(game, 'A VEN') == 'ITALY' + assert self.owner_name(game, 'F ION') == 'ITALY' + + def test_6_d_4(self): + """ 6.D.4. TEST CASE, SUPPORT TO HOLD ON UNIT SUPPORTING A HOLD ALLOWED + A unit that is supporting a hold, can receive a hold support. + Germany: A Berlin Supports F Kiel + Germany: F Kiel Supports A Berlin + Russia: F Baltic Sea Supports A Prussia - Berlin + Russia: A Prussia - Berlin + The Russian move from Prussia to Berlin fails. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'F KIE']) + self.set_units(game, 'RUSSIA', ['F BAL', 'A PRU']) + self.set_orders(game, 'GERMANY', ['A BER S F KIE', 'F KIE S A BER']) + self.set_orders(game, 'RUSSIA', ['F BAL S A PRU - BER', 'A PRU - BER']) + self.process(game) + assert self.check_results(game, 'A BER', 'cut') + assert self.check_results(game, 'F KIE', '') + assert self.check_results(game, 'F BAL', '') + assert self.check_results(game, 'A PRU', 'bounce') + assert self.owner_name(game, 'A BER') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'F BAL') == 'RUSSIA' + assert self.owner_name(game, 'A PRU') == 'RUSSIA' + + def test_6_d_5(self): + """ 6.D.5. TEST CASE, SUPPORT TO HOLD ON UNIT SUPPORTING A MOVE ALLOWED + A unit that is supporting a move, can receive a hold support. + Germany: A Berlin Supports A Munich - Silesia + Germany: F Kiel Supports A Berlin + Germany: A Munich - Silesia + Russia: F Baltic Sea Supports A Prussia - Berlin + Russia: A Prussia - Berlin + The Russian move from Prussia to Berlin fails. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'F KIE', 'A MUN']) + self.set_units(game, 'RUSSIA', ['F BAL', 'A PRU']) + self.set_orders(game, 'GERMANY', ['A BER S A MUN - SIL', 'F KIE S A BER', 'A MUN - SIL']) + self.set_orders(game, 'RUSSIA', ['F BAL S A PRU - BER', 'A PRU - BER']) + self.process(game) + assert self.check_results(game, 'A BER', 'cut') + assert self.check_results(game, 'F KIE', '') + assert self.check_results(game, 'A MUN', '') + assert self.check_results(game, 'F BAL', '') + assert self.check_results(game, 'A PRU', 'bounce') + assert self.owner_name(game, 'A BER') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'A MUN') is None + assert self.owner_name(game, 'F BAL') == 'RUSSIA' + assert self.owner_name(game, 'A PRU') == 'RUSSIA' + assert self.owner_name(game, 'A SIL') == 'GERMANY' + + def test_6_d_6(self): + """ 6.D.6. TEST CASE, SUPPORT TO HOLD ON CONVOYING UNIT ALLOWED + A unit that is convoying, can receive a hold support. + Germany: A Berlin - Sweden + Germany: F Baltic Sea Convoys A Berlin - Sweden + Germany: F Prussia Supports F Baltic Sea + Russia: F Livonia - Baltic Sea + Russia: F Gulf of Bothnia Supports F Livonia - Baltic Sea + The Russian move from Livonia to the Baltic Sea fails. The convoy from Berlin to Sweden succeeds. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'F BAL', 'F PRU']) + self.set_units(game, 'RUSSIA', ['F LVN', 'F BOT']) + self.set_orders(game, 'GERMANY', ['A BER - SWE', 'F BAL C A BER - SWE', 'F PRU S F BAL']) + self.set_orders(game, 'RUSSIA', ['F LVN - BAL', 'F BOT S F LVN - BAL']) + self.process(game) + assert self.check_results(game, 'A BER', '') + assert self.check_results(game, 'F BAL', '') + assert self.check_results(game, 'F PRU', '') + assert self.check_results(game, 'F LVN', 'bounce') + assert self.check_results(game, 'F BOT', '') + assert self.owner_name(game, 'A BER') is None + assert self.owner_name(game, 'F BAL') == 'GERMANY' + assert self.owner_name(game, 'F PRU') == 'GERMANY' + assert self.owner_name(game, 'F LVN') == 'RUSSIA' + assert self.owner_name(game, 'F BOT') == 'RUSSIA' + assert self.owner_name(game, 'A SWE') == 'GERMANY' + + def test_6_d_7(self): + """ 6.D.7. TEST CASE, SUPPORT TO HOLD ON MOVING UNIT NOT ALLOWED + A unit that is moving, can not receive a hold support for the situation that the move fails. + Germany: F Baltic Sea - Sweden + Germany: F Prussia Supports F Baltic Sea + Russia: F Livonia - Baltic Sea + Russia: F Gulf of Bothnia Supports F Livonia - Baltic Sea + Russia: A Finland - Sweden + The support of the fleet in Prussia fails. The fleet in Baltic Sea will bounce on the Russian army + in Finland and will be dislodged by the Russian fleet from Livonia when it returns to the Baltic Sea. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['F BAL', 'F PRU']) + self.set_units(game, 'RUSSIA', ['F LVN', 'F BOT', 'A FIN']) + self.set_orders(game, 'GERMANY', ['F BAL - SWE', 'F PRU S F BAL']) + self.set_orders(game, 'RUSSIA', ['F LVN - BAL', 'F BOT S F LVN - BAL', 'A FIN - SWE']) + self.process(game) + assert self.check_results(game, 'F BAL', 'bounce') + assert self.check_results(game, 'F BAL', 'dislodged') + assert self.check_results(game, 'F PRU', 'void') + assert self.check_results(game, 'F LVN', '') + assert self.check_results(game, 'F BOT', '') + assert self.check_results(game, 'A FIN', 'bounce') + assert check_dislodged(game, 'F BAL', 'F LVN') + assert self.owner_name(game, 'F BAL') == 'RUSSIA' + assert self.owner_name(game, 'F PRU') == 'GERMANY' + assert self.owner_name(game, 'F LVN') is None + assert self.owner_name(game, 'F BOT') == 'RUSSIA' + assert self.owner_name(game, 'A FIN') == 'RUSSIA' + assert self.owner_name(game, 'A SWE') is None + + def test_6_d_8(self): + """ 6.D.8. TEST CASE, FAILED CONVOY CAN NOT RECEIVE HOLD SUPPORT + If a convoy fails because of disruption of the convoy or when the right convoy orders are not given, + then the army to be convoyed can not receive support in hold, since it still tried to move. + Austria: F Ionian Sea Hold + Austria: A Serbia Supports A Albania - Greece + Austria: A Albania - Greece + Turkey: A Greece - Naples + Turkey: A Bulgaria Supports A Greece + There was a possible convoy from Greece to Naples, before the orders were made public (via the Ionian Sea). + This means that the order of Greece to Naples should never be treated as illegal order and be changed in a + hold order able to receive hold support (see also issue VI.A). Therefore, the support in Bulgaria fails and + the army in Greece is dislodged by the army in Albania. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['F ION', 'A SER', 'A ALB']) + self.set_units(game, 'TURKEY', ['A GRE', 'A BUL']) + self.set_orders(game, 'AUSTRIA', ['F ION H', 'A SER S A ALB - GRE', 'A ALB - GRE']) + self.set_orders(game, 'TURKEY', ['A GRE - NAP', 'A BUL S A GRE']) + self.process(game) + assert self.check_results(game, 'F ION', '') + assert self.check_results(game, 'A SER', '') + assert self.check_results(game, 'A ALB', '') + assert self.check_results(game, 'A GRE', 'dislodged') + assert self.check_results(game, 'A BUL', 'void') + assert check_dislodged(game, 'A GRE', 'A ALB') + assert self.owner_name(game, 'F ION') == 'AUSTRIA' + assert self.owner_name(game, 'A SER') == 'AUSTRIA' + assert self.owner_name(game, 'A ALB') is None + assert self.owner_name(game, 'A GRE') == 'AUSTRIA' + assert self.owner_name(game, 'A BUL') == 'TURKEY' + + def test_6_d_9(self): + """ 6.D.9. TEST CASE, SUPPORT TO MOVE ON HOLDING UNIT NOT ALLOWED + A unit that is holding can not receive a support in moving. + Italy: A Venice - Trieste + Italy: A Tyrolia Supports A Venice - Trieste + Austria: A Albania Supports A Trieste - Serbia + Austria: A Trieste Hold + The support of the army in Albania fails and the army in Trieste is dislodged by the army from Venice. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ITALY', ['A VEN', 'A TYR']) + self.set_units(game, 'AUSTRIA', ['A ALB', 'A TRI']) + self.set_orders(game, 'ITALY', ['A VEN - TRI', 'A TYR S A VEN - TRI']) + self.set_orders(game, 'AUSTRIA', ['A ALB S A TRI - SER', 'A TRI H']) + self.process(game) + assert self.check_results(game, 'A VEN', '') + assert self.check_results(game, 'A TYR', '') + assert self.check_results(game, 'A ALB', 'void') + assert self.check_results(game, 'A TRI', 'dislodged') + assert check_dislodged(game, 'A TRI', 'A VEN') + assert self.owner_name(game, 'A VEN') is None + assert self.owner_name(game, 'A TYR') == 'ITALY' + assert self.owner_name(game, 'A ALB') == 'AUSTRIA' + assert self.owner_name(game, 'A TRI') == 'ITALY' + + def test_6_d_10(self): + """ 6.D.10. TEST CASE, SELF DISLODGMENT PROHIBITED + A unit may not dislodge a unit of the same great power. + Germany: A Berlin Hold + Germany: F Kiel - Berlin + Germany: A Munich Supports F Kiel - Berlin + Move to Berlin fails. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'F KIE', 'A MUN']) + self.set_orders(game, 'GERMANY', ['A BER H', 'F KIE - BER', 'A MUN S F KIE - BER']) + self.process(game) + assert self.check_results(game, 'A BER', '') + assert self.check_results(game, 'F KIE', 'bounce') + assert self.check_results(game, 'A MUN', 'void') + assert self.owner_name(game, 'A BER') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'A MUN') == 'GERMANY' + + def test_6_d_11(self): + """ 6.D.11. TEST CASE, NO SELF DISLODGMENT OF RETURNING UNIT + Idem. + Germany: A Berlin - Prussia + Germany: F Kiel - Berlin + Germany: A Munich Supports F Kiel - Berlin + Russia: A Warsaw - Prussia + Army in Berlin bounces, but is not dislodged by own unit. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'F KIE', 'A MUN']) + self.set_units(game, 'RUSSIA', 'A WAR') + self.set_orders(game, 'GERMANY', ['A BER - PRU', 'F KIE - BER', 'A MUN S F KIE - BER']) + self.set_orders(game, 'RUSSIA', ['A WAR - PRU']) + self.process(game) + assert self.check_results(game, 'A BER', 'bounce') + assert self.check_results(game, 'F KIE', 'bounce') + assert self.check_results(game, 'A MUN', 'void') + assert self.check_results(game, 'A WAR', 'bounce') + assert self.owner_name(game, 'A BER') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'A WAR') == 'RUSSIA' + + def test_6_d_12(self): + """ 6.D.12. TEST CASE, SUPPORTING A FOREIGN UNIT TO DISLODGE OWN UNIT PROHIBITED + You may not help another power in dislodging your own unit. + Austria: F Trieste Hold + Austria: A Vienna Supports A Venice - Trieste + Italy: A Venice - Trieste + No dislodgment of fleet in Trieste. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['F TRI', 'A VIE']) + self.set_units(game, 'ITALY', ['A VEN']) + self.set_orders(game, 'AUSTRIA', ['F TRI H', 'A VIE S A VEN - TRI']) + self.set_orders(game, 'ITALY', 'A VEN - TRI') + self.process(game) + assert self.check_results(game, 'F TRI', '') + assert self.check_results(game, 'A VIE', 'void') + assert self.check_results(game, 'A VEN', 'bounce') + assert self.owner_name(game, 'F TRI') == 'AUSTRIA' + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A VEN') == 'ITALY' + + def test_6_d_13(self): + """ 6.D.13. TEST CASE, SUPPORTING A FOREIGN UNIT TO DISLODGE A RETURNING OWN UNIT PROHIBITED + Idem. + Austria: F Trieste - Adriatic Sea + Austria: A Vienna Supports A Venice - Trieste + Italy: A Venice - Trieste + Italy: F Apulia - Adriatic Sea + No dislodgment of fleet in Trieste. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['F TRI', 'A VIE']) + self.set_units(game, 'ITALY', ['A VEN', 'F APU']) + self.set_orders(game, 'AUSTRIA', ['F TRI - ADR', 'A VIE S A VEN - TRI']) + self.set_orders(game, 'ITALY', ['A VEN - TRI', 'F APU - ADR']) + self.process(game) + assert self.check_results(game, 'F TRI', 'bounce') + assert self.check_results(game, 'A VIE', 'void') + assert self.check_results(game, 'A VEN', 'bounce') + assert self.check_results(game, 'F APU', 'bounce') + assert self.owner_name(game, 'F TRI') == 'AUSTRIA' + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A VEN') == 'ITALY' + assert self.owner_name(game, 'F APU') == 'ITALY' + + def test_6_d_14(self): + """ 6.D.14. TEST CASE, SUPPORTING A FOREIGN UNIT IS NOT ENOUGH TO PREVENT DISLODGEMENT + If a foreign unit has enough support to dislodge your unit, you may not prevent that dislodgement by + supporting the attack. + Austria: F Trieste Hold + Austria: A Vienna Supports A Venice - Trieste + Italy: A Venice - Trieste + Italy: A Tyrolia Supports A Venice - Trieste + Italy: F Adriatic Sea Supports A Venice - Trieste + The fleet in Trieste is dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['F TRI', 'A VIE']) + self.set_units(game, 'ITALY', ['A VEN', 'A TYR', 'F ADR']) + self.set_orders(game, 'AUSTRIA', ['F TRI H', 'A VIE S A VEN - TRI']) + self.set_orders(game, 'ITALY', ['A VEN - TRI', 'A TUR S A VEN - TRI', 'F ADR S A VEN - TRI']) + self.process(game) + assert self.check_results(game, 'F TRI', 'dislodged') + assert self.check_results(game, 'A VIE', 'void') + assert self.check_results(game, 'A VEN', '') + assert self.check_results(game, 'A TYR', '') + assert self.check_results(game, 'F ADR', '') + assert check_dislodged(game, 'F TRI', 'A VEN') + assert self.owner_name(game, 'A TRI') == 'ITALY' + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A VEN') is None + assert self.owner_name(game, 'A TYR') == 'ITALY' + assert self.owner_name(game, 'F ADR') == 'ITALY' + + def test_6_d_15(self): + """ 6.D.15. TEST CASE, DEFENDER CAN NOT CUT SUPPORT FOR ATTACK ON ITSELF + A unit that is attacked by a supported unit can not prevent dislodgement by guessing which of the units + will do the support. + Russia: F Constantinople Supports F Black Sea - Ankara + Russia: F Black Sea - Ankara + Turkey: F Ankara - Constantinople + The support of Constantinople is not cut and the fleet in Ankara is dislodged by the fleet in the Black Sea. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'RUSSIA', ['F CON', 'F BLA']) + self.set_units(game, 'TURKEY', ['F ANK']) + self.set_orders(game, 'RUSSIA', ['F CON S F BLA - ANK', 'F BLA - ANK']) + self.set_orders(game, 'TURKEY', ['F ANK - CON']) + self.process(game) + assert self.check_results(game, 'F CON', '') + assert self.check_results(game, 'F BLA', '') + assert self.check_results(game, 'F ANK', 'bounce') + assert self.check_results(game, 'F ANK', 'dislodged') + assert check_dislodged(game, 'F ANK', 'F BLA') + assert self.owner_name(game, 'F CON') == 'RUSSIA' + assert self.owner_name(game, 'F BLA') is None + assert self.owner_name(game, 'F ANK') == 'RUSSIA' + + def test_6_d_16(self): + """ 6.D.16. TEST CASE, CONVOYING A UNIT DISLODGING A UNIT OF SAME POWER IS ALLOWED + It is allowed to convoy a foreign unit that dislodges your own unit is allowed. + England: A London Hold + England: F North Sea Convoys A Belgium - London + France: F English Channel Supports A Belgium - London + France: A Belgium - London + The English army in London is dislodged by the French army coming from Belgium. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A LON', 'F NTH']) + self.set_units(game, 'FRANCE', ['F ENG', 'A BEL']) + self.set_orders(game, 'ENGLAND', ['A LON H', 'F NTH C A BEL - LON']) + self.set_orders(game, 'FRANCE', ['F ENG S A BEL - LON', 'A BEL - LON']) + self.process(game) + assert self.check_results(game, 'A LON', 'dislodged') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'A BEL', '') + assert check_dislodged(game, 'A LON', 'A BEL') + assert self.owner_name(game, 'A LON') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'A BEL') is None + + def test_6_d_17(self): + """ 6.D.17. TEST CASE, DISLODGEMENT CUTS SUPPORTS + The famous dislodge rule. + Russia: F Constantinople Supports F Black Sea - Ankara + Russia: F Black Sea - Ankara + Turkey: F Ankara - Constantinople + Turkey: A Smyrna Supports F Ankara - Constantinople + Turkey: A Armenia - Ankara + The Russian fleet in Constantinople is dislodged. This cuts the support to from Black Sea to Ankara. + Black Sea will bounce with the army from Armenia. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'RUSSIA', ['F CON', 'F BLA']) + self.set_units(game, 'TURKEY', ['F ANK', 'A SMY', 'A ARM']) + self.set_orders(game, 'RUSSIA', ['F CON S F BLA - ANK', 'F BLA - ANK']) + self.set_orders(game, 'TURKEY', ['F ANK - CON', 'A SMY S F ANK - CON', 'A ARM - ANK']) + self.process(game) + assert self.check_results(game, 'F CON', 'dislodged') + assert self.check_results(game, 'F CON', 'cut') + assert self.check_results(game, 'F BLA', 'bounce') + assert self.check_results(game, 'F ANK', '') + assert self.check_results(game, 'A SMY', '') + assert self.check_results(game, 'A ARM', 'bounce') + assert check_dislodged(game, 'F CON', 'F ANK') + assert self.owner_name(game, 'F CON') == 'TURKEY' + assert self.owner_name(game, 'F BLA') == 'RUSSIA' + assert self.owner_name(game, 'F ANK') is None + assert self.owner_name(game, 'A SMY') == 'TURKEY' + assert self.owner_name(game, 'A ARM') == 'TURKEY' + + def test_6_d_18(self): + """ 6.D.18. TEST CASE, A SURVIVING UNIT WILL SUSTAIN SUPPORT + Idem. But now with an additional hold that prevents dislodgement. + Russia: F Constantinople Supports F Black Sea - Ankara + Russia: F Black Sea - Ankara + Russia: A Bulgaria Supports F Constantinople + Turkey: F Ankara - Constantinople + Turkey: A Smyrna Supports F Ankara - Constantinople + Turkey: A Armenia - Ankara + The Russian fleet in the Black Sea will dislodge the Turkish fleet in Ankara. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'RUSSIA', ['F CON', 'F BLA', 'A BUL']) + self.set_units(game, 'TURKEY', ['F ANK', 'A SMY', 'A ARM']) + self.set_orders(game, 'RUSSIA', ['F CON S F BLA - ANK', 'F BLA - ANK', 'A BUL S F CON']) + self.set_orders(game, 'TURKEY', ['F ANK - CON', 'A SMY S F ANK - CON', 'A ARM - ANK']) + self.process(game) + assert self.check_results(game, 'F CON', '') + assert self.check_results(game, 'F BLA', '') + assert self.check_results(game, 'A BUL', '') + assert self.check_results(game, 'F ANK', 'dislodged') + assert self.check_results(game, 'A SMY', '') + assert self.check_results(game, 'A ARM', 'bounce') + assert check_dislodged(game, 'F ANK', 'F BLA') + assert self.owner_name(game, 'F CON') == 'RUSSIA' + assert self.owner_name(game, 'F BLA') is None + assert self.owner_name(game, 'A BUL') == 'RUSSIA' + assert self.owner_name(game, 'F ANK') == 'RUSSIA' + assert self.owner_name(game, 'A SMY') == 'TURKEY' + assert self.owner_name(game, 'A ARM') == 'TURKEY' + + def test_6_d_19(self): + """ 6.D.19. TEST CASE, EVEN WHEN SURVIVING IS IN ALTERNATIVE WAY + Now, the dislodgement is prevented because the supports comes from a Russian army: + Russia: F Constantinople Supports F Black Sea - Ankara + Russia: F Black Sea - Ankara + Russia: A Smyrna Supports F Ankara - Constantinople + Turkey: F Ankara - Constantinople + The Russian fleet in Constantinople is not dislodged, because one of the support is of Russian origin. + The support from Black Sea to Ankara will sustain and the fleet in Ankara will be dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'RUSSIA', ['F CON', 'F BLA', 'A SMY']) + self.set_units(game, 'TURKEY', ['F ANK']) + self.set_orders(game, 'RUSSIA', ['F CON S F BLA - ANK', 'F BLA - ANK', 'A SMY S F ANK - CON']) + self.set_orders(game, 'TURKEY', 'F ANK - CON') + self.process(game) + assert self.check_results(game, 'F CON', '') + assert self.check_results(game, 'F BLA', '') + assert self.check_results(game, 'A SMY', 'void') + assert self.check_results(game, 'F ANK', 'dislodged') + assert check_dislodged(game, 'F ANK', 'F BLA') + assert self.owner_name(game, 'F CON') == 'RUSSIA' + assert self.owner_name(game, 'F BLA') is None + assert self.owner_name(game, 'A SMY') == 'RUSSIA' + assert self.owner_name(game, 'F ANK') == 'RUSSIA' + + def test_6_d_20(self): + """ 6.D.20. TEST CASE, UNIT CAN NOT CUT SUPPORT OF ITS OWN COUNTRY + Although this is not mentioned in all rulebooks, it is generally accepted that when a unit attacks + another unit of the same Great Power, it will not cut support. + England: F London Supports F North Sea - English Channel + England: F North Sea - English Channel + England: A Yorkshire - London + France: F English Channel Hold + The army in York does not cut support. This means that the fleet in the English Channel is dislodged by the + fleet in the North Sea. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F LON', 'F NTH', 'A YOR']) + self.set_units(game, 'FRANCE', 'F ENG') + self.set_orders(game, 'ENGLAND', ['F LON S F NTH - ENG', 'F NTH - ENG', 'A YOR - LON']) + self.set_orders(game, 'FRANCE', 'F ENG H') + self.process(game) + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A YOR', 'bounce') + assert self.check_results(game, 'F ENG', 'dislodged') + assert check_dislodged(game, 'F ENG', 'F NTH') + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'F NTH') is None + assert self.owner_name(game, 'A YOR') == 'ENGLAND' + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + + def test_6_d_21(self): + """ 6.D.21. TEST CASE, DISLODGING DOES NOT CANCEL A SUPPORT CUT + Sometimes there is the question whether a dislodged moving unit does not cut support (similar to the + dislodge rule). This is not the case. + Austria: F Trieste Hold + Italy: A Venice - Trieste + Italy: A Tyrolia Supports A Venice - Trieste + Germany: A Munich - Tyrolia + Russia: A Silesia - Munich + Russia: A Berlin Supports A Silesia - Munich + Although the German army is dislodged, it still cuts the Italian support. That means that the Austrian + Fleet is not dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['F TRI']) + self.set_units(game, 'ITALY', ['A VEN', 'A TYR']) + self.set_units(game, 'GERMANY', 'A MUN') + self.set_units(game, 'RUSSIA', ['A SIL', 'A BER']) + self.set_orders(game, 'AUSTRIA', 'F TRI H') + self.set_orders(game, 'ITALY', ['A VEN - TRI', 'A TYR S A VEN - TRI']) + self.set_orders(game, 'GERMANY', 'A MUN - TYR') + self.set_orders(game, 'RUSSIA', ['A SIL - MUN', 'A BER S A SIL - MUN']) + self.process(game) + assert self.check_results(game, 'F TRI', '') + assert self.check_results(game, 'A VEN', 'bounce') + assert self.check_results(game, 'A TYR', 'cut') + assert self.check_results(game, 'A MUN', 'dislodged') + assert self.check_results(game, 'A SIL', '') + assert self.check_results(game, 'A BER', '') + assert check_dislodged(game, 'A MUN', 'A SIL') + assert self.owner_name(game, 'F TRI') == 'AUSTRIA' + assert self.owner_name(game, 'A VEN') == 'ITALY' + assert self.owner_name(game, 'A TYR') == 'ITALY' + assert self.owner_name(game, 'A MUN') == 'RUSSIA' + assert self.owner_name(game, 'A SIL') is None + assert self.owner_name(game, 'A BER') == 'RUSSIA' + + def test_6_d_22(self): + """ 6.D.22. TEST CASE, IMPOSSIBLE FLEET MOVE CAN NOT BE SUPPORTED + If a fleet tries moves to a land area it seems pointless to support the fleet, since the move will fail + anyway. However, in such case, the support is also invalid for defense purposes. + Germany: F Kiel - Munich + Germany: A Burgundy Supports F Kiel - Munich + Russia: A Munich - Kiel + Russia: A Berlin Supports A Munich - Kiel + The German move from Kiel to Munich is illegal (fleets can not go to Munich). Therefore, the support from + Burgundy fails and the Russian army in Munich will dislodge the fleet in Kiel. Note that the failing of the + support is not explicitly mentioned in the rulebooks (the DPTG is more clear about this point). If you take + the rulebooks very literally, you might conclude that the fleet in Munich is not dislodged, but this is an + incorrect interpretation. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['F KIE', 'A BUR']) + self.set_units(game, 'RUSSIA', ['A MUN', 'A BER']) + self.set_orders(game, 'GERMANY', ['F KIE - MUN', 'A BUR S F KIE - MUN']) + self.set_orders(game, 'RUSSIA', ['A MUN - KIE', 'A BER S A MUN - KIE']) + self.process(game) + assert self.check_results(game, 'F KIE', 'void') + assert self.check_results(game, 'F KIE', 'dislodged') + assert self.check_results(game, 'A BUR', 'void') + assert self.check_results(game, 'A MUN', '') + assert self.check_results(game, 'A BER', '') + assert check_dislodged(game, 'F KIE', 'A MUN') + assert self.owner_name(game, 'A KIE') == 'RUSSIA' + assert self.owner_name(game, 'A BUR') == 'GERMANY' + assert self.owner_name(game, 'A MUN') is None + assert self.owner_name(game, 'A BER') == 'RUSSIA' + + def test_6_d_23(self): + """ 6.D.23. TEST CASE, IMPOSSIBLE COAST MOVE CAN NOT BE SUPPORTED + Comparable with the previous test case, but now the fleet move is impossible for coastal reasons. + Italy: F Gulf of Lyon - Spain(sc) + Italy: F Western Mediterranean Supports F Gulf of Lyon - Spain(sc) + France: F Spain(nc) - Gulf of Lyon + France: F Marseilles Supports F Spain(nc) - Gulf of Lyon + The French move from Spain North Coast to Gulf of Lyon is illegal (wrong coast). Therefore, the support + from Marseilles fails and the fleet in Spain is dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ITALY', ['F LYO', 'F WES']) + self.set_units(game, 'FRANCE', ['F SPA/NC', 'F MAR']) + self.set_orders(game, 'ITALY', ['F LYO - SPA/SC', 'F WES S F LYO - SPA/SC']) + self.set_orders(game, 'FRANCE', ['F SPA/NC - LYO', 'F MAR S F SPA/NC - LYO']) + self.process(game) + assert self.check_results(game, 'F LYO', '') + assert self.check_results(game, 'F WES', '') + assert self.check_results(game, 'F SPA/NC', 'void') + assert self.check_results(game, 'F SPA/NC', 'dislodged') + assert self.check_results(game, 'F MAR', 'void') + assert check_dislodged(game, 'F SPA/NC', 'F LYO') + assert self.owner_name(game, 'F LYO') is None + assert self.owner_name(game, 'F WES') == 'ITALY' + assert self.owner_name(game, 'F SPA/NC') is None + assert self.owner_name(game, 'F SPA/SC') == 'ITALY' + assert self.owner_name(game, 'F MAR') == 'FRANCE' + + def test_6_d_24(self): + """ 6.D.24. TEST CASE, IMPOSSIBLE ARMY MOVE CAN NOT BE SUPPORTED + Comparable with the previous test case, but now an army tries to move into sea and the support is used in a + beleaguered garrison. + France: A Marseilles - Gulf of Lyon + France: F Spain(sc) Supports A Marseilles - Gulf of Lyon + Italy: F Gulf of Lyon Hold + Turkey: F Tyrrhenian Sea Supports F Western Mediterranean - Gulf of Lyon + Turkey: F Western Mediterranean - Gulf of Lyon + The French move from Marseilles to Gulf of Lyon is illegal (an army can not go to sea). Therefore, + the support from Spain fails and there is no beleaguered garrison. The fleet in the Gulf of Lyon is + dislodged by the Turkish fleet in the Western Mediterranean. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['A MAR', 'F SPA/SC']) + self.set_units(game, 'ITALY', ['F LYO']) + self.set_units(game, 'TURKEY', ['F TYS', 'F WES']) + self.set_orders(game, 'FRANCE', ['A MAR - LYO', 'F SPA/SC S A MAR - LYO']) + self.set_orders(game, 'ITALY', ['F LYO H']) + self.set_orders(game, 'TURKEY', ['F TYS S F WES - LYO', 'F WES - LYO']) + self.process(game) + assert self.check_results(game, 'A MAR', 'void') + assert self.check_results(game, 'F SPA/SC', 'void') + assert self.check_results(game, 'F LYO', 'dislodged') + assert self.check_results(game, 'F TYS', '') + assert self.check_results(game, 'F WES', '') + assert check_dislodged(game, 'F LYO', 'F WES') + assert self.owner_name(game, 'A MAR') == 'FRANCE' + assert self.owner_name(game, 'F SPA/SC') == 'FRANCE' + assert self.owner_name(game, 'F LYO') == 'TURKEY' + assert self.owner_name(game, 'F TYS') == 'TURKEY' + assert self.owner_name(game, 'F WES') is None + + def test_6_d_25(self): + """ 6.D.25. TEST CASE, FAILING HOLD SUPPORT CAN BE SUPPORTED + If an adjudicator fails on one of the previous three test cases, then the bug should be removed with care. + A failing move can not be supported, but a failing hold support, because of some preconditions (unmatching + order) can still be supported. + Germany: A Berlin Supports A Prussia + Germany: F Kiel Supports A Berlin + Russia: F Baltic Sea Supports A Prussia - Berlin + Russia: A Prussia - Berlin + Although the support of Berlin on Prussia fails (because of unmatching orders), the support of Kiel on + Berlin is still valid. So, Berlin will not be dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'F KIE']) + self.set_units(game, 'RUSSIA', ['F BAL', 'A PRU']) + self.set_orders(game, 'GERMANY', ['A BER S A PRU', 'F KIE S A BER']) + self.set_orders(game, 'RUSSIA', ['F BAL S A PRU - BER', 'A PRU - BER']) + self.process(game) + assert self.check_results(game, 'A BER', 'void') + assert self.check_results(game, 'F KIE', '') + assert self.check_results(game, 'F BAL', '') + assert self.check_results(game, 'A PRU', 'bounce') + assert self.owner_name(game, 'A BER') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'F BAL') == 'RUSSIA' + assert self.owner_name(game, 'A PRU') == 'RUSSIA' + + def test_6_d_26(self): + """ 6.D.26. TEST CASE, FAILING MOVE SUPPORT CAN BE SUPPORTED + Similar as the previous test case, but now with an unmatched support to move. + Germany: A Berlin Supports A Prussia - Silesia + Germany: F Kiel Supports A Berlin + Russia: F Baltic Sea Supports A Prussia - Berlin + Russia: A Prussia - Berlin + Again, Berlin will not be dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'F KIE']) + self.set_units(game, 'RUSSIA', ['F BAL', 'A PRU']) + self.set_orders(game, 'GERMANY', ['A BER S A PRU - SIL', 'F KIE S A BER']) + self.set_orders(game, 'RUSSIA', ['F BAL S A PRU - BER', 'A PRU - BER']) + self.process(game) + assert self.check_results(game, 'A BER', 'void') + assert self.check_results(game, 'F KIE', '') + assert self.check_results(game, 'F BAL', '') + assert self.check_results(game, 'A PRU', 'bounce') + assert self.owner_name(game, 'A BER') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'F BAL') == 'RUSSIA' + assert self.owner_name(game, 'A PRU') == 'RUSSIA' + + def test_6_d_27(self): + """ 6.D.27. TEST CASE, FAILING CONVOY CAN BE SUPPORTED + Similar as the previous test case, but now with an unmatched convoy. + England: F Sweden - Baltic Sea + England: F Denmark Supports F Sweden - Baltic Sea + Germany: A Berlin Hold + Russia: F Baltic Sea Convoys A Berlin - Livonia + Russia: F Prussia Supports F Baltic Sea + The convoy order in the Baltic Sea is unmatched and fails. However, the support of Prussia on the Baltic Sea + is still valid and the fleet in the Baltic Sea is not dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F SWE', 'F DEN']) + self.set_units(game, 'GERMANY', 'A BER') + self.set_units(game, 'RUSSIA', ['F BAL', 'F PRU']) + self.set_orders(game, 'ENGLAND', ['F SWE - BAL', 'F DEN S F SWE - BAL']) + self.set_orders(game, 'GERMANY', 'A BER H') + self.set_orders(game, 'RUSSIA', ['F BAL C A BER - LVN', 'F PRU S F BAL']) + self.process(game) + assert self.check_results(game, 'F SWE', 'bounce') + assert self.check_results(game, 'F DEN', '') + assert self.check_results(game, 'A BER', '') + assert self.check_results(game, 'F BAL', 'void') + assert self.check_results(game, 'F PRU', '') + assert self.owner_name(game, 'F SWE') == 'ENGLAND' + assert self.owner_name(game, 'F DEN') == 'ENGLAND' + assert self.owner_name(game, 'A BER') == 'GERMANY' + assert self.owner_name(game, 'F BAL') == 'RUSSIA' + assert self.owner_name(game, 'F PRU') == 'RUSSIA' + + def test_6_d_28(self): + """ 6.D.28. TEST CASE, IMPOSSIBLE MOVE AND SUPPORT + If a move is impossible then it can be treated as "illegal", which makes a hold support possible. + Austria: A Budapest Supports F Rumania + Russia: F Rumania - Holland + Turkey: F Black Sea - Rumania + Turkey: A Bulgaria Supports F Black Sea - Rumania + The move of the Russian fleet is impossible. But the question is, whether it is "illegal" (see issue 4.E.1). + If the move is "illegal" it must be ignored and that makes the hold support of the army in Budapest valid + and the fleet in Rumania will not be dislodged. + I prefer that the move is "illegal", which means that the fleet in the Black Sea does not dislodge the + supported Russian fleet. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['A BUD']) + self.set_units(game, 'RUSSIA', 'F RUM') + self.set_units(game, 'TURKEY', ['F BLA', 'A BUL']) + self.set_orders(game, 'AUSTRIA', 'A BUD S F RUM') + self.set_orders(game, 'RUSSIA', 'F RUM - HOL') + self.set_orders(game, 'TURKEY', ['F BLA - RUM', 'A BUL S F BLA - RUM']) + self.process(game) + assert self.check_results(game, 'A BUD', '') + assert self.check_results(game, 'F RUM', 'void') + assert self.check_results(game, 'F BLA', 'bounce') + assert self.check_results(game, 'A BUL', '') + assert self.owner_name(game, 'A BUD') == 'AUSTRIA' + assert self.owner_name(game, 'F RUM') == 'RUSSIA' + assert self.owner_name(game, 'F BLA') == 'TURKEY' + assert self.owner_name(game, 'A BUL') == 'TURKEY' + + def test_6_d_29(self): + """ 6.D.29. TEST CASE, MOVE TO IMPOSSIBLE COAST AND SUPPORT + Similar to the previous test case, but now the move can be "illegal" because of the wrong coast. + Austria: A Budapest Supports F Rumania + Russia: F Rumania - Bulgaria(sc) + Turkey: F Black Sea - Rumania + Turkey: A Bulgaria Supports F Black Sea - Rumania + Again the move of the Russian fleet is impossible. However, some people might correct the coast + (see issue 4.B.3). If the coast is not corrected, again the question is whether it is "illegal" (see + issue 4.E.1). If the move is "illegal" it must be ignored and that makes the hold support of the army in + Budapest valid and the fleet in Rumania will not be dislodged. + I prefer that unambiguous orders are not changed and that the move is "illegal". That means that the fleet + in the Black Sea does not dislodge the supported Russian fleet. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', 'A BUD') + self.set_units(game, 'RUSSIA', 'F RUM') + self.set_units(game, 'TURKEY', ['F BLA', 'A BUL']) + self.set_orders(game, 'AUSTRIA', 'A BUD S F RUM') + self.set_orders(game, 'RUSSIA', 'F RUM - BUL/SC') + self.set_orders(game, 'TURKEY', ['F BLA - RUM', 'A BUL S F BLA - RUM']) + self.process(game) + assert self.check_results(game, 'A BUD', '') + assert self.check_results(game, 'F RUM', 'void') + assert self.check_results(game, 'F BLA', 'bounce') + assert self.check_results(game, 'A BUL', '') + assert self.owner_name(game, 'A BUD') == 'AUSTRIA' + assert self.owner_name(game, 'F RUM') == 'RUSSIA' + assert self.owner_name(game, 'F BLA') == 'TURKEY' + assert self.owner_name(game, 'A BUL') == 'TURKEY' + + def test_6_d_30(self): + """ 6.D.30. TEST CASE, MOVE WITHOUT COAST AND SUPPORT + Similar to the previous test case, but now the move can be "illegal" because of missing coast. + Italy: F Aegean Sea Supports F Constantinople + Russia: F Constantinople - Bulgaria + Turkey: F Black Sea - Constantinople + Turkey: A Bulgaria Supports F Black Sea - Constantinople + Again the order to the Russian fleet is with problems, because it does not specify the coast, while both + coasts of Bulgaria are possible. If no default coast is taken (see issue 4.B.1), then also here it must be + decided whether the order is "illegal" (see issue 4.E.1). If the move is "illegal" it must be ignored and + that makes the hold support of the fleet in the Aegean Sea valid and the Russian fleet will not be + dislodged. I don't like default coasts and I prefer that the move is "illegal". That means that the fleet + in the Black Sea does not dislodge the supported Russian fleet. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ITALY', 'F AEG') + self.set_units(game, 'RUSSIA', 'F CON') + self.set_units(game, 'TURKEY', ['F BLA', 'A BUL']) + self.set_orders(game, 'ITALY', ['F AEG S F CON']) + self.set_orders(game, 'RUSSIA', ['F CON - BUL']) + self.set_orders(game, 'TURKEY', ['F BLA - CON', 'A BUL S F BLA - CON']) + self.process(game) + assert self.check_results(game, 'F AEG', '') + assert self.check_results(game, 'F CON', 'void') + assert self.check_results(game, 'F BLA', 'bounce') + assert self.check_results(game, 'A BUL', '') + assert self.owner_name(game, 'F AEG') == 'ITALY' + assert self.owner_name(game, 'F CON') == 'RUSSIA' + assert self.owner_name(game, 'F BLA') == 'TURKEY' + assert self.owner_name(game, 'A BUL') == 'TURKEY' + + def test_6_d_31(self): + """ 6.D.31. TEST CASE, A TRICKY IMPOSSIBLE SUPPORT + A support order can be impossible for complex reasons. + Austria: A Rumania - Armenia + Turkey: F Black Sea Supports A Rumania - Armenia + Although the army in Rumania can move to Armenia and the fleet in the Black Sea can also go to Armenia, + the support is still not possible. The reason is that the only possible convoy is through the Black Sea and + a fleet can not convoy and support at the same time. + This is relevant for computer programs that show only the possible orders. In the list of possible orders, + the support as given to the fleet in the Black Sea, should not be listed. Furthermore, if the fleet in the + Black Sea gets a second order, then this may fail, because of double orders (although it can also be ruled + differently, see issue 4.D.3). However, when the support order is considered "illegal" (see issue 4.E.1), + then this impossible support must be ignored and the second order must be carried out. + I prefer that impossible orders are "illegal" and ignored. If there would be a second order for the fleet + in the Black Sea, that order should be carried out. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', 'A RUM') + self.set_units(game, 'TURKEY', 'F BLA') + self.set_orders(game, 'AUSTRIA', ['A RUM - ARM']) + self.set_orders(game, 'TURKEY', ['F BLA S A RUM - ARM']) + self.process(game) + assert self.check_results(game, 'A RUM', 'no convoy') + assert self.check_results(game, 'F BLA', 'void') + assert self.owner_name(game, 'A RUM') == 'AUSTRIA' + assert self.owner_name(game, 'F BLA') == 'TURKEY' + + def test_6_d_32(self): + """ 6.D.32. TEST CASE, A MISSING FLEET + The previous test cases contained an order that was impossible even when some other pieces on the board + where changed. In this test case, the order is impossible, but only for that situation. + England: F Edinburgh Supports A Liverpool - Yorkshire + England: A Liverpool - Yorkshire + France: F London Supports A Yorkshire + Germany: A Yorkshire - Holland + The German order to Yorkshire can not be executed, because there is no fleet in the North Sea. In other + situations (where there is a fleet in the North Sea), the exact same order would be possible. It should be + determined whether this is "illegal" (see issue 4.E.1) or not. If it is illegal, then the order should be + ignored and the support of the French fleet in London succeeds. This means that the army in Yorkshire is + not dislodged. + I prefer that impossible orders, even if it is only impossible for the current situation, are "illegal" and + ignored. The army in Yorkshire is not dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F EDI', 'A LVP']) + self.set_units(game, 'FRANCE', 'F LON') + self.set_units(game, 'GERMANY', 'A YOR') + self.set_orders(game, 'ENGLAND', ['F EDI S A LVP - YOR', 'A LVP - YOR']) + self.set_orders(game, 'FRANCE', ['F LON S A YOR']) + self.set_orders(game, 'GERMANY', ['A YOR - HOL']) + self.process(game) + assert self.check_results(game, 'F EDI', '') + assert self.check_results(game, 'A LVP', 'bounce') + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'A YOR', 'void') + assert self.owner_name(game, 'F EDI') == 'ENGLAND' + assert self.owner_name(game, 'A LVP') == 'ENGLAND' + assert self.owner_name(game, 'F LON') == 'FRANCE' + assert self.owner_name(game, 'A YOR') == 'GERMANY' + + def test_6_d_33(self): + """ 6.D.33. TEST CASE, UNWANTED SUPPORT ALLOWED + A self stand-off can be broken by an unwanted support. + Austria: A Serbia - Budapest + Austria: A Vienna - Budapest + Russia: A Galicia Supports A Serbia - Budapest + Turkey: A Bulgaria - Serbia + Due to the Russian support, the army in Serbia advances to Budapest. This enables Turkey to capture + Serbia with the army in Bulgaria. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['A SER', 'A VIE']) + self.set_units(game, 'RUSSIA', 'A GAL') + self.set_units(game, 'TURKEY', 'A BUL') + self.set_orders(game, 'AUSTRIA', ['A SER - BUD', 'A VIE - BUD']) + self.set_orders(game, 'RUSSIA', 'A GAL S A SER - BUD') + self.set_orders(game, 'TURKEY', 'A BUL - SER') + self.process(game) + assert self.check_results(game, 'A SER', '') + assert self.check_results(game, 'A VIE', 'bounce') + assert self.check_results(game, 'A GAL', '') + assert self.check_results(game, 'A BUL', '') + assert self.owner_name(game, 'A SER') == 'TURKEY' + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A GAL') == 'RUSSIA' + assert self.owner_name(game, 'A BUL') is None + assert self.owner_name(game, 'A BUD') == 'AUSTRIA' + + def test_6_d_34(self): + """ 6.D.34. TEST CASE, SUPPORT TARGETING OWN AREA NOT ALLOWED + Support targeting the area where the supporting unit is standing, is illegal. + Germany: A Berlin - Prussia + Germany: A Silesia Supports A Berlin - Prussia + Germany: F Baltic Sea Supports A Berlin - Prussia + Italy: A Prussia Supports Livonia - Prussia + Russia: A Warsaw Supports A Livonia - Prussia + Russia: A Livonia - Prussia + Russia and Italy wanted to get rid of the Italian army in Prussia (to build an Italian fleet somewhere + else). However, they didn't want a possible German attack on Prussia to succeed. They invented this odd + order of Italy. It was intended that the attack of the army in Livonia would have strength three, so it + would be capable to prevent the possible German attack to succeed. However, the order of Italy is illegal, + because a unit may only support to an area where the unit can go by itself. A unit can't go to the area it + is already standing, so the Italian order is illegal and the German move from Berlin succeeds. Even if it + would be legal, the German move from Berlin would still succeed, because the support of Prussia is cut by + Livonia and Berlin. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'A SIL', 'F BAL']) + self.set_units(game, 'ITALY', 'A PRU') + self.set_units(game, 'RUSSIA', ['A WAR', 'A LVN']) + self.set_orders(game, 'GERMANY', ['A BER - PRU', 'A SIL S A BER - PRU', 'F BAL S A BER - PRU']) + self.set_orders(game, 'ITALY', ['A PRU S LVN - PRU']) + self.set_orders(game, 'RUSSIA', ['A WAR S A LVN - PRU', 'A LVN - PRU']) + self.process(game) + assert self.check_results(game, 'A BER', '') + assert self.check_results(game, 'A SIL', '') + assert self.check_results(game, 'F BAL', '') + assert self.check_results(game, 'A PRU', 'void') + assert self.check_results(game, 'A PRU', 'dislodged') + assert self.check_results(game, 'A WAR', '') + assert self.check_results(game, 'A LVN', 'bounce') + assert check_dislodged(game, 'A PRU', 'A BER') + assert self.owner_name(game, 'A BER') is None + assert self.owner_name(game, 'A SIL') == 'GERMANY' + assert self.owner_name(game, 'F BAL') == 'GERMANY' + assert self.owner_name(game, 'A PRU') == 'GERMANY' + assert self.owner_name(game, 'A WAR') == 'RUSSIA' + assert self.owner_name(game, 'A LVN') == 'RUSSIA' + + # 6.E. TEST CASES, HEAD TO HEAD BATTLES AND BELEAGUERED GARRISON + def test_6_e_1(self): + """ 6.E.1. TEST CASE, DISLODGED UNIT HAS NO EFFECT ON ATTACKERS AREA + An army can follow. + Germany: A Berlin - Prussia + Germany: F Kiel - Berlin + Germany: A Silesia Supports A Berlin - Prussia + Russia: A Prussia - Berlin + The army in Kiel will move to Berlin. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'F KIE', 'A SIL']) + self.set_units(game, 'RUSSIA', 'A PRU') + self.set_orders(game, 'GERMANY', ['A BER - PRU', 'F KIE - BER', 'A SIL S A BER - PRU']) + self.set_orders(game, 'RUSSIA', 'A PRU - BER') + self.process(game) + assert self.check_results(game, 'A BER', '') + assert self.check_results(game, 'F KIE', '') + assert self.check_results(game, 'A SIL', '') + assert self.check_results(game, 'A PRU', 'dislodged') + assert self.check_results(game, 'A PRU', 'bounce') + assert check_dislodged(game, 'A PRU', 'A BER') + assert self.owner_name(game, 'F BER') == 'GERMANY' + assert self.owner_name(game, 'F KIE') is None + assert self.owner_name(game, 'A SIL') == 'GERMANY' + assert self.owner_name(game, 'A PRU') == 'GERMANY' + + def test_6_e_2(self): + """ 6.E.2. TEST CASE, NO SELF DISLODGEMENT IN HEAD TO HEAD BATTLE + Self dislodgement is not allowed. This also counts for head to head battles. + Germany: A Berlin - Kiel + Germany: F Kiel - Berlin + Germany: A Munich Supports A Berlin - Kiel + No unit will move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'F KIE', 'A MUN']) + self.set_orders(game, 'GERMANY', ['A BER - KIE', 'F KIE - BER', 'A MUN S A BER - KIE']) + self.process(game) + assert self.check_results(game, 'A BER', 'bounce') + assert self.check_results(game, 'F KIE', 'bounce') + assert self.check_results(game, 'A MUN', 'void') + assert self.owner_name(game, 'A BER') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'A MUN') == 'GERMANY' + + def test_6_e_3(self): + """ 6.E.3. TEST CASE, NO HELP IN DISLODGING OWN UNIT + To help a foreign power to dislodge own unit in head to head battle is not possible. + Germany: A Berlin - Kiel + Germany: A Munich Supports F Kiel - Berlin + England: F Kiel - Berlin + No unit will move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['A BER', 'A MUN']) + self.set_units(game, 'ENGLAND', 'F KIE') + self.set_orders(game, 'GERMANY', ['A BER - KIE', 'A MUN S F KIE - BER']) + self.set_orders(game, 'ENGLAND', 'F KIE - BER') + self.process(game) + assert self.check_results(game, 'A BER', 'bounce') + assert self.check_results(game, 'A MUN', 'void') + assert self.check_results(game, 'F KIE', 'bounce') + assert self.owner_name(game, 'A BER') == 'GERMANY' + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'ENGLAND' + + def test_6_e_4(self): + """ 6.E.4. TEST CASE, NON-DISLODGED LOSER HAS STILL EFFECT + If in an unbalanced head to head battle the loser is not dislodged, it has still effect on the area of + the attacker. + Germany: F Holland - North Sea + Germany: F Helgoland Bight Supports F Holland - North Sea + Germany: F Skagerrak Supports F Holland - North Sea + France: F North Sea - Holland + France: F Belgium Supports F North Sea - Holland + England: F Edinburgh Supports F Norwegian Sea - North Sea + England: F Yorkshire Supports F Norwegian Sea - North Sea + England: F Norwegian Sea - North Sea + Austria: A Kiel Supports A Ruhr - Holland + Austria: A Ruhr - Holland + The French fleet in the North Sea is not dislodged due to the beleaguered garrison. Therefore, + the Austrian army in Ruhr will not move to Holland. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['F HOL', 'F HEL', 'F SKA']) + self.set_units(game, 'FRANCE', ['F NTH', 'F BEL']) + self.set_units(game, 'ENGLAND', ['F EDI', 'F YOR', 'F NWG']) + self.set_units(game, 'AUSTRIA', ['A KIE', 'A RUH']) + self.set_orders(game, 'GERMANY', ['F HOL - NTH', 'F HEL S F HOL - NTH', 'F SKA S F HOL - NTH']) + self.set_orders(game, 'FRANCE', ['F NTH - HOL', 'F BEL S F NTH - HOL']) + self.set_orders(game, 'ENGLAND', ['F EDI S F NWG - NTH', 'F YOR S F NWG - NTH', 'F NWG - NTH']) + self.set_orders(game, 'AUSTRIA', ['A KIE S A RUH - HOL', 'A RUH - HOL']) + self.process(game) + assert self.check_results(game, 'F HOL', 'bounce') + assert self.check_results(game, 'F HEL', '') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'F NTH', 'bounce') + assert self.check_results(game, 'F BEL', '') + assert self.check_results(game, 'F EDI', '') + assert self.check_results(game, 'F YOR', '') + assert self.check_results(game, 'F NWG', 'bounce') + assert self.check_results(game, 'A KIE', '') + assert self.check_results(game, 'A RUH', 'bounce') + assert self.owner_name(game, 'F HOL') == 'GERMANY' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F SKA') == 'GERMANY' + assert self.owner_name(game, 'F NTH') == 'FRANCE' + assert self.owner_name(game, 'F BEL') == 'FRANCE' + assert self.owner_name(game, 'F EDI') == 'ENGLAND' + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'F NWG') == 'ENGLAND' + assert self.owner_name(game, 'A KIE') == 'AUSTRIA' + assert self.owner_name(game, 'A RUH') == 'AUSTRIA' + + def test_6_e_5(self): + """ 6.E.5. TEST CASE, LOSER DISLODGED BY ANOTHER ARMY HAS STILL EFFECT + If in an unbalanced head to head battle the loser is dislodged by a unit not part of the head to head + battle, the loser has still effect on the place of the winner of the head to head battle. + Germany: F Holland - North Sea + Germany: F Helgoland Bight Supports F Holland - North Sea + Germany: F Skagerrak Supports F Holland - North Sea + France: F North Sea - Holland + France: F Belgium Supports F North Sea - Holland + England: F Edinburgh Supports F Norwegian Sea - North Sea + England: F Yorkshire Supports F Norwegian Sea - North Sea + England: F Norwegian Sea - North Sea + England: F London Supports F Norwegian Sea - North Sea + Austria: A Kiel Supports A Ruhr - Holland + Austria: A Ruhr - Holland + The French fleet in the North Sea is dislodged but not by the German fleet in Holland. Therefore, + the French fleet can still prevent that the Austrian army in Ruhr will move to Holland. So, the Austrian + move in Ruhr fails and the German fleet in Holland is not dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['F HOL', 'F HEL', 'F SKA']) + self.set_units(game, 'FRANCE', ['F NTH', 'F BEL']) + self.set_units(game, 'ENGLAND', ['F EDI', 'F YOR', 'F NWG', 'F LON']) + self.set_units(game, 'AUSTRIA', ['A KIE', 'A RUH']) + self.set_orders(game, 'GERMANY', ['F HOL - NTH', 'F HEL S F HOL - NTH', 'F SKA S F HOL - NTH']) + self.set_orders(game, 'FRANCE', ['F NTH - HOL', 'F BEL S F NTH - HOL']) + self.set_orders(game, 'ENGLAND', ['F EDI S F NWG - NTH', 'F YOR S F NWG - NTH', 'F NWG - NTH', + 'F LON S F NWG - NTH']) + self.set_orders(game, 'AUSTRIA', ['A KIE S A RUH - HOL', 'A RUH - HOL']) + self.process(game) + assert self.check_results(game, 'F HOL', 'bounce') + assert self.check_results(game, 'F HEL', '') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'F NTH', 'dislodged') + assert self.check_results(game, 'F NTH', 'bounce') + assert self.check_results(game, 'F BEL', '') + assert self.check_results(game, 'F EDI', '') + assert self.check_results(game, 'F YOR', '') + assert self.check_results(game, 'F NWG', '') + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'A KIE', '') + assert self.check_results(game, 'A RUH', 'bounce') + assert check_dislodged(game, 'F NTH', 'F NWG') + assert self.owner_name(game, 'F HOL') == 'GERMANY' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F SKA') == 'GERMANY' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'F BEL') == 'FRANCE' + assert self.owner_name(game, 'F EDI') == 'ENGLAND' + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'F NWG') is None + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'A KIE') == 'AUSTRIA' + assert self.owner_name(game, 'A RUH') == 'AUSTRIA' + + def test_6_e_6(self): + """ 6.E.6. TEST CASE, NOT DISLODGE BECAUSE OF OWN SUPPORT HAS STILL EFFECT + If in an unbalanced head to head battle the loser is not dislodged because the winner had help of a unit + of the loser, the loser has still effect on the area of the winner. + Germany: F Holland - North Sea + Germany: F Helgoland Bight Supports F Holland - North Sea + France: F North Sea - Holland + France: F Belgium Supports F North Sea - Holland + France: F English Channel Supports F Holland - North Sea + Austria: A Kiel Supports A Ruhr - Holland + Austria: A Ruhr - Holland + Although the German force from Holland to North Sea is one larger than the French force from North Sea + to Holland, + the French fleet in the North Sea is not dislodged, because one of the supports on the German movement is + French. + Therefore, the Austrian army in Ruhr will not move to Holland. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'GERMANY', ['F HOL', 'F HEL']) + self.set_units(game, 'FRANCE', ['F NTH', 'F BEL', 'F ENG']) + self.set_units(game, 'AUSTRIA', ['A KIE', 'A RUH']) + self.set_orders(game, 'GERMANY', ['F HOL - NTH', 'F HEL S F HOL - NTH']) + self.set_orders(game, 'FRANCE', ['F NTH - HOL', 'F BEL S F NTH - HOL', 'F ENG S F HOL - NTH']) + self.set_orders(game, 'AUSTRIA', ['A KIE S A RUH - HOL', 'A RUH - HOL']) + self.process(game) + assert self.check_results(game, 'F HOL', 'bounce') + assert self.check_results(game, 'F HEL', '') + assert self.check_results(game, 'F NTH', 'bounce') + assert self.check_results(game, 'F BEL', '') + assert self.check_results(game, 'F ENG', 'void') + assert self.check_results(game, 'A KIE', '') + assert self.check_results(game, 'A RUH', 'bounce') + assert self.owner_name(game, 'F HOL') == 'GERMANY' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F NTH') == 'FRANCE' + assert self.owner_name(game, 'F BEL') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'A KIE') == 'AUSTRIA' + assert self.owner_name(game, 'A RUH') == 'AUSTRIA' + + def test_6_e_7(self): + """ 6.E.7. TEST CASE, NO SELF DISLODGEMENT WITH BELEAGUERED GARRISON + An attempt to self dislodgement can be combined with a beleaguered garrison. Such self dislodgment is still + not possible. + England: F North Sea Hold + England: F Yorkshire Supports F Norway - North Sea + Germany: F Holland Supports F Helgoland Bight - North Sea + Germany: F Helgoland Bight - North Sea + Russia: F Skagerrak Supports F Norway - North Sea + Russia: F Norway - North Sea + Although the Russians beat the German attack (with the support of Yorkshire) and the two Russian fleets + are enough to dislodge the fleet in the North Sea, the fleet in the North Sea is not dislodged, since it + would not be dislodged if the English fleet in Yorkshire would not give support. According to the DPTG the + fleet in the North Sea would be dislodged. The DPTG is incorrect in this case. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'F YOR']) + self.set_units(game, 'GERMANY', ['F HOL', 'F HEL']) + self.set_units(game, 'RUSSIA', ['F SKA', 'F NWY']) + self.set_orders(game, 'ENGLAND', ['F NTH H', 'F YOR S F NWY - NTH']) + self.set_orders(game, 'GERMANY', ['F HOL S F HEL - NTH', 'F HEL - NTH']) + self.set_orders(game, 'RUSSIA', ['F SKA S F NWY - NTH', 'F NWY - NTH']) + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'F YOR', 'void') + assert self.check_results(game, 'F HOL', '') + assert self.check_results(game, 'F HEL', 'bounce') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'F NWY', 'bounce') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'F HOL') == 'GERMANY' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F SKA') == 'RUSSIA' + assert self.owner_name(game, 'F NWY') == 'RUSSIA' + + def test_6_e_8(self): + """ 6.E.8. TEST CASE, NO SELF DISLODGEMENT WITH BELEAGUERED GARRISON AND HEAD TO HEAD BATTLE + Similar to the previous test case, but now the beleaguered fleet is also engaged in a head to head battle. + England: F North Sea - Norway + England: F Yorkshire Supports F Norway - North Sea + Germany: F Holland Supports F Helgoland Bight - North Sea + Germany: F Helgoland Bight - North Sea + Russia: F Skagerrak Supports F Norway - North Sea + Russia: F Norway - North Sea + Again, none of the fleets move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'F YOR']) + self.set_units(game, 'GERMANY', ['F HOL', 'F HEL']) + self.set_units(game, 'RUSSIA', ['F SKA', 'F NWY']) + self.set_orders(game, 'ENGLAND', ['F NTH - NWY', 'F YOR S F NWY - NTH']) + self.set_orders(game, 'GERMANY', ['F HOL S F HEL - NTH', 'F HEL - NTH']) + self.set_orders(game, 'RUSSIA', ['F SKA S F NWY - NTH', 'F NWY - NTH']) + self.process(game) + assert self.check_results(game, 'F NTH', 'bounce') + assert self.check_results(game, 'F YOR', 'void') + assert self.check_results(game, 'F HOL', '') + assert self.check_results(game, 'F HEL', 'bounce') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'F NWY', 'bounce') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'F HOL') == 'GERMANY' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F SKA') == 'RUSSIA' + assert self.owner_name(game, 'F NWY') == 'RUSSIA' + + def test_6_e_9(self): + """ 6.E.9. TEST CASE, ALMOST SELF DISLODGEMENT WITH BELEAGUERED GARRISON + Similar to the previous test case, but now the beleaguered fleet is moving away. + England: F North Sea - Norwegian Sea + England: F Yorkshire Supports F Norway - North Sea + Germany: F Holland Supports F Helgoland Bight - North Sea + Germany: F Helgoland Bight - North Sea + Russia: F Skagerrak Supports F Norway - North Sea + Russia: F Norway - North Sea + Both the fleet in the North Sea and the fleet in Norway move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'F YOR']) + self.set_units(game, 'GERMANY', ['F HOL', 'F HEL']) + self.set_units(game, 'RUSSIA', ['F SKA', 'F NWY']) + self.set_orders(game, 'ENGLAND', ['F NTH - NWG', 'F YOR S F NWY - NTH']) + self.set_orders(game, 'GERMANY', ['F HOL S F HEL - NTH', 'F HEL - NTH']) + self.set_orders(game, 'RUSSIA', ['F SKA S F NWY - NTH', 'F NWY - NTH']) + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'F YOR', '') + assert self.check_results(game, 'F HOL', '') + assert self.check_results(game, 'F HEL', 'bounce') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'F NWY', '') + assert self.owner_name(game, 'F NTH') == 'RUSSIA' + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'F HOL') == 'GERMANY' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F SKA') == 'RUSSIA' + assert self.owner_name(game, 'F NWY') is None + assert self.owner_name(game, 'F NWG') == 'ENGLAND' + + def test_6_e_10(self): + """ 6.E.10. TEST CASE, ALMOST CIRCULAR MOVEMENT WITH NO SELF DISLODGEMENT WITH BELEAGUERED GARRISON + Similar to the previous test case, but now the beleaguered fleet is in circular movement with the weaker + attacker. So, the circular movement fails. + England: F North Sea - Denmark + England: F Yorkshire Supports F Norway - North Sea + Germany: F Holland Supports F Helgoland Bight - North Sea + Germany: F Helgoland Bight - North Sea + Germany: F Denmark - Helgoland Bight + Russia: F Skagerrak Supports F Norway - North Sea + Russia: F Norway - North Sea + There is no movement of fleets. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'F YOR']) + self.set_units(game, 'GERMANY', ['F HOL', 'F HEL', 'F DEN']) + self.set_units(game, 'RUSSIA', ['F SKA', 'F NWY']) + self.set_orders(game, 'ENGLAND', ['F NTH - DEN', 'F YOR S F NWY - NTH']) + self.set_orders(game, 'GERMANY', ['F HOL S F HEL - NTH', 'F HEL - NTH', 'F DEN - HEL']) + self.set_orders(game, 'RUSSIA', ['F SKA S F NWY - NTH', 'F NWY - NTH']) + self.process(game) + assert self.check_results(game, 'F NTH', 'bounce') + assert self.check_results(game, 'F YOR', 'void') + assert self.check_results(game, 'F HOL', '') + assert self.check_results(game, 'F HEL', 'bounce') + assert self.check_results(game, 'F DEN', 'bounce') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'F NWY', 'bounce') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'F HOL') == 'GERMANY' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F DEN') == 'GERMANY' + assert self.owner_name(game, 'F SKA') == 'RUSSIA' + assert self.owner_name(game, 'F NWY') == 'RUSSIA' + + def test_6_e_11(self): + """ 6.E.11. TEST CASE, NO SELF DISLODGEMENT WITH BELEAGUERED GARRISON, UNIT SWAP WITH ADJACENT CONVOYING AND + TWO COASTS + Similar to the previous test case, but now the beleaguered fleet is in a unit swap with the stronger + attacker. So, the unit swap succeeds. To make the situation more complex, the swap is on an area with + two coasts. + France: A Spain - Portugal via Convoy + France: F Mid-Atlantic Ocean Convoys A Spain - Portugal + France: F Gulf of Lyon Supports F Portugal - Spain(nc) + Germany: A Marseilles Supports A Gascony - Spain + Germany: A Gascony - Spain + Italy: F Portugal - Spain(nc) + Italy: F Western Mediterranean Supports F Portugal - Spain(nc) + The unit swap succeeds. Note that due to the success of the swap, there is no beleaguered garrison anymore. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['A SPA', 'F MAO', 'F LYO']) + self.set_units(game, 'GERMANY', ['A MAR', 'A GAS']) + self.set_units(game, 'ITALY', ['F POR', 'F WES']) + self.set_orders(game, 'FRANCE', ['A SPA - POR VIA', 'F MAO C A SPA - POR', 'F LYO S F POR - SPA/NC']) + self.set_orders(game, 'GERMANY', ['A MAR S A GAS - SPA', 'A GAS - SPA']) + self.set_orders(game, 'ITALY', ['F POR - SPA/NC', 'F WES S F POR - SPA/NC']) + self.process(game) + assert self.check_results(game, 'A SPA', '') + assert self.check_results(game, 'F MAO', '') + assert self.check_results(game, 'F LYO', '') + assert self.check_results(game, 'A MAR', '') + assert self.check_results(game, 'A GAS', 'bounce') + assert self.check_results(game, 'F POR', '') + assert self.check_results(game, 'F WES', '') + assert self.owner_name(game, 'F SPA') == 'ITALY' + assert self.owner_name(game, 'F SPA/NC') == 'ITALY' + assert self.owner_name(game, 'F SPA/SC') is None + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'F LYO') == 'FRANCE' + assert self.owner_name(game, 'A MAR') == 'GERMANY' + assert self.owner_name(game, 'A GAS') == 'GERMANY' + assert self.owner_name(game, 'A POR') == 'FRANCE' + assert self.owner_name(game, 'F WES') == 'ITALY' + + def test_6_e_12(self): + """ 6.E.12. TEST CASE, SUPPORT ON ATTACK ON OWN UNIT CAN BE USED FOR OTHER MEANS + A support on an attack on your own unit has still effect. It can prevent that another army will dislodge + the unit. + Austria: A Budapest - Rumania + Austria: A Serbia Supports A Vienna - Budapest + Italy: A Vienna - Budapest + Russia: A Galicia - Budapest + Russia: A Rumania Supports A Galicia - Budapest + The support of Serbia on the Italian army prevents that the Russian army in Galicia will advance. + No army will move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['A BUD', 'A SER']) + self.set_units(game, 'ITALY', ['A VIE']) + self.set_units(game, 'RUSSIA', ['A GAL', 'A RUM']) + self.set_orders(game, 'AUSTRIA', ['A BUD - RUM', 'A SER S A VIE - BUD']) + self.set_orders(game, 'ITALY', 'A VIE - BUD') + self.set_orders(game, 'RUSSIA', ['A GAL - BUD', 'A RUM S A GAL - BUD']) + self.process(game) + assert self.check_results(game, 'A BUD', 'bounce') + assert self.check_results(game, 'A SER', '') + assert self.check_results(game, 'A VIE', 'bounce') + assert self.check_results(game, 'A GAL', 'bounce') + assert self.check_results(game, 'A RUM', '') + assert self.owner_name(game, 'A BUD') == 'AUSTRIA' + assert self.owner_name(game, 'A SER') == 'AUSTRIA' + assert self.owner_name(game, 'A VIE') == 'ITALY' + assert self.owner_name(game, 'A GAL') == 'RUSSIA' + assert self.owner_name(game, 'A RUM') == 'RUSSIA' + + def test_6_e_13(self): + """ 6.E.13. TEST CASE, THREE WAY BELEAGUERED GARRISON + In a beleaguered garrison from three sides, the adjudicator may not let two attacks fail and then let the + third succeed. + England: F Edinburgh Supports F Yorkshire - North Sea + England: F Yorkshire - North Sea + France: F Belgium - North Sea + France: F English Channel Supports F Belgium - North Sea + Germany: F North Sea Hold + Russia: F Norwegian Sea - North Sea + Russia: F Norway Supports F Norwegian Sea - North Sea + None of the fleets move. The German fleet in the North Sea is not dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F EDI', 'F YOR']) + self.set_units(game, 'FRANCE', ['F BEL', 'F ENG']) + self.set_units(game, 'GERMANY', 'F NTH') + self.set_units(game, 'RUSSIA', ['F NWG', 'F NWY']) + self.set_orders(game, 'ENGLAND', ['F EDI S F YOR - NTH', 'F YOR - NTH']) + self.set_orders(game, 'FRANCE', ['F BEL - NTH', 'F ENG S F BEL - NTH']) + self.set_orders(game, 'GERMANY', 'F NTH H') + self.set_orders(game, 'RUSSIA', ['F NWG - NTH', 'F NWY S F NWG - NTH']) + self.process(game) + assert self.check_results(game, 'F EDI', '') + assert self.check_results(game, 'F YOR', 'bounce') + assert self.check_results(game, 'F BEL', 'bounce') + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'F NWG', 'bounce') + assert self.check_results(game, 'F NWY', '') + assert self.owner_name(game, 'F EDI') == 'ENGLAND' + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'F BEL') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'GERMANY' + assert self.owner_name(game, 'F NWG') == 'RUSSIA' + assert self.owner_name(game, 'F NWY') == 'RUSSIA' + + def test_6_e_14(self): + """ 6.E.14. TEST CASE, ILLEGAL HEAD TO HEAD BATTLE CAN STILL DEFEND + If in a head to head battle, one of the units makes an illegal move, than that unit has still the + possibility to defend against attacks with strength of one. + England: A Liverpool - Edinburgh + Russia: F Edinburgh - Liverpool + The move of the Russian fleet is illegal, but can still prevent the English army to enter Edinburgh. So, + none of the units move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A LVP']) + self.set_units(game, 'RUSSIA', ['F EDI']) + self.set_orders(game, 'ENGLAND', ['A LVP - EDI']) + self.set_orders(game, 'RUSSIA', ['F EDI - LVP']) + self.process(game) + assert self.check_results(game, 'A LVP', 'bounce') + assert self.check_results(game, 'F EDI', 'void') + assert self.owner_name(game, 'A LVP') == 'ENGLAND' + assert self.owner_name(game, 'F EDI') == 'RUSSIA' + + def test_6_e_15(self): + """ 6.E.15. TEST CASE, THE FRIENDLY HEAD TO HEAD BATTLE + In this case both units in the head to head battle prevent that the other one is dislodged. + England: F Holland Supports A Ruhr - Kiel + England: A Ruhr - Kiel + France: A Kiel - Berlin + France: A Munich Supports A Kiel - Berlin + France: A Silesia Supports A Kiel - Berlin + Germany: A Berlin - Kiel + Germany: F Denmark Supports A Berlin - Kiel + Germany: F Helgoland Bight Supports A Berlin - Kiel + Russia: F Baltic Sea Supports A Prussia - Berlin + Russia: A Prussia - Berlin + None of the moves succeeds. This case is especially difficult for sequence based adjudicators. They will + start adjudicating the head to head battle and continue to adjudicate the attack on one of the units part + of the head to head battle. In this self.process, one of the sides of the head to head battle might be + cancelled out. This happens in the DPTG. If this is adjudicated according to the DPTG, the unit in Ruhr or + in Prussia will advance (depending on the order the units are adjudicated). This is clearly a bug in the + DPTG. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F HOL', 'A RUH']) + self.set_units(game, 'FRANCE', ['A KIE', 'A MUN', 'A SIL']) + self.set_units(game, 'GERMANY', ['A BER', 'F DEN', 'F HEL']) + self.set_units(game, 'RUSSIA', ['F BAL', 'A PRU']) + self.set_orders(game, 'ENGLAND', ['F HOL S A RUH - KIE', 'A RUH - KIE']) + self.set_orders(game, 'FRANCE', ['A KIE - BER', 'A MUN S A KIE - BER', 'A SIL S A KIE - BER']) + self.set_orders(game, 'GERMANY', ['A BER - KIE', 'F DEN S A BER - KIE', 'F HEL S A BER - KIE']) + self.set_orders(game, 'RUSSIA', ['F BAL S A PRU - BER', 'A PRU - BER']) + self.process(game) + assert self.check_results(game, 'F HOL', '') + assert self.check_results(game, 'A RUH', 'bounce') + assert self.check_results(game, 'A KIE', 'bounce') + assert self.check_results(game, 'A MUN', '') + assert self.check_results(game, 'A SIL', '') + assert self.check_results(game, 'A BER', 'bounce') + assert self.check_results(game, 'F DEN', '') + assert self.check_results(game, 'F HEL', '') + assert self.check_results(game, 'F BAL', '') + assert self.check_results(game, 'A PRU', 'bounce') + assert self.owner_name(game, 'F HOL') + assert self.owner_name(game, 'A RUH') + assert self.owner_name(game, 'A KIE') + assert self.owner_name(game, 'A MUN') + assert self.owner_name(game, 'A SIL') + assert self.owner_name(game, 'A BER') + assert self.owner_name(game, 'F DEN') + assert self.owner_name(game, 'F HEL') + assert self.owner_name(game, 'F BAL') + assert self.owner_name(game, 'A PRU') + + # 6.F. TEST CASES, CONVOYS + def test_6_f_1(self): + """ 6.F.1. TEST CASE, NO CONVOY IN COASTAL AREAS + A fleet in a coastal area may not convoy. + Turkey: A Greece - Sevastopol + Turkey: F Aegean Sea Convoys A Greece - Sevastopol + Turkey: F Constantinople Convoys A Greece - Sevastopol + Turkey: F Black Sea Convoys A Greece - Sevastopol + The convoy in Constantinople is not possible. So, the army in Greece will not move to Sevastopol. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'TURKEY', ['A GRE', 'F AEG', 'F CON', 'F BLA']) + self.set_orders(game, 'TURKEY', ['A GRE - SEV', 'F AEG C A GRE - SEV', 'F CON C A GRE - SEV', + 'F BLA C A GRE - SEV']) + self.process(game) + # Note F CON is void, the other moves then are impossible (i.e. A GRE can't move by convoy to SEV) + assert self.check_results(game, 'A GRE', 'void') + assert self.check_results(game, 'F AEG', 'void') + assert self.check_results(game, 'F CON', 'void') + assert self.check_results(game, 'F BLA', 'void') + assert self.owner_name(game, 'A GRE') == 'TURKEY' + assert self.owner_name(game, 'F AEG') == 'TURKEY' + assert self.owner_name(game, 'F CON') == 'TURKEY' + assert self.owner_name(game, 'F BLA') == 'TURKEY' + assert self.owner_name(game, 'A SEV') is None + + def test_6_f_2(self): + """ 6.F.2. TEST CASE, AN ARMY BEING CONVOYED CAN BOUNCE AS NORMAL + Armies being convoyed bounce on other units just as armies that are not being convoyed. + England: F English Channel Convoys A London - Brest + England: A London - Brest + France: A Paris - Brest + The English army in London bounces on the French army in Paris. Both units do not move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F ENG', 'A LON']) + self.set_units(game, 'FRANCE', 'A PAR') + self.set_orders(game, 'ENGLAND', ['F ENG C A LON - BRE', 'A LON - BRE']) + self.set_orders(game, 'FRANCE', 'A PAR - BRE') + self.process(game) + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'A LON', 'bounce') + assert self.check_results(game, 'A PAR', 'bounce') + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + assert self.owner_name(game, 'A LON') == 'ENGLAND' + assert self.owner_name(game, 'A PAR') == 'FRANCE' + + def test_6_f_3(self): + """ 6.F.3. TEST CASE, AN ARMY BEING CONVOYED CAN RECEIVE SUPPORT + Armies being convoyed can receive support as in any other move. + England: F English Channel Convoys A London - Brest + England: A London - Brest + England: F Mid-Atlantic Ocean Supports A London - Brest + France: A Paris - Brest + The army in London receives support and beats the army in Paris. This means that the army London will end + in Brest and the French army in Paris stays in Paris. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F ENG', 'A LON', 'F MAO']) + self.set_units(game, 'FRANCE', 'A PAR') + self.set_orders(game, 'ENGLAND', ['F ENG C A LON - BRE', 'A LON - BRE', 'F MAO S A LON - BRE']) + self.set_orders(game, 'FRANCE', 'A PAR - BRE') + self.process(game) + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'F MAO', '') + assert self.check_results(game, 'A PAR', 'bounce') + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + assert self.owner_name(game, 'A BRE') == 'ENGLAND' + assert self.owner_name(game, 'F MAO') == 'ENGLAND' + assert self.owner_name(game, 'A LON') is None + assert self.owner_name(game, 'A PAR') == 'FRANCE' + + def test_6_f_4(self): + """ 6.F.4. TEST CASE, AN ATTACKED CONVOY IS NOT DISRUPTED + A convoy can only be disrupted by dislodging the fleets. Attacking is not sufficient. + England: F North Sea Convoys A London - Holland + England: A London - Holland + Germany: F Skagerrak - North Sea + The army in London will successfully convoy and end in Holland. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A LON']) + self.set_units(game, 'GERMANY', 'F SKA') + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - HOL', 'A LON - HOL']) + self.set_orders(game, 'GERMANY', 'F SKA - NTH') + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'F SKA', 'bounce') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A LON') is None + assert self.owner_name(game, 'F SKA') == 'GERMANY' + assert self.owner_name(game, 'A HOL') == 'ENGLAND' + + def test_6_f_5(self): + """ 6.F.5. TEST CASE, A BELEAGUERED CONVOY IS NOT DISRUPTED + Even when a convoy is in a beleaguered garrison it is not disrupted. + England: F North Sea Convoys A London - Holland + England: A London - Holland + France: F English Channel - North Sea + France: F Belgium Supports F English Channel - North Sea + Germany: F Skagerrak - North Sea + Germany: F Denmark Supports F Skagerrak - North Sea + The army in London will successfully convoy and end in Holland. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A LON']) + self.set_units(game, 'FRANCE', ['F ENG', 'F BEL']) + self.set_units(game, 'GERMANY', ['F SKA', 'F DEN']) + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - HOL', 'A LON - HOL']) + self.set_orders(game, 'FRANCE', ['F ENG - NTH', 'F BEL S F ENG - NTH']) + self.set_orders(game, 'GERMANY', ['F SKA - NTH', 'F DEN S F SKA - NTH']) + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'F ENG', 'bounce') + assert self.check_results(game, 'F BEL', '') + assert self.check_results(game, 'F SKA', 'bounce') + assert self.check_results(game, 'F DEN', '') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A LON') is None + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F BEL') == 'FRANCE' + assert self.owner_name(game, 'F SKA') == 'GERMANY' + assert self.owner_name(game, 'F DEN') == 'GERMANY' + assert self.owner_name(game, 'A HOL') == 'ENGLAND' + + def test_6_f_6(self): + """ 6.F.6. TEST CASE, DISLODGED CONVOY DOES NOT CUT SUPPORT + When a fleet of a convoy is dislodged, the convoy is completely cancelled. So, no support is cut. + England: F North Sea Convoys A London - Holland + England: A London - Holland + Germany: A Holland Supports A Belgium + Germany: A Belgium Supports A Holland + Germany: F Helgoland Bight Supports F Skagerrak - North Sea + Germany: F Skagerrak - North Sea + France: A Picardy - Belgium + France: A Burgundy Supports A Picardy - Belgium + The hold order of Holland on Belgium will sustain and Belgium will not be dislodged by the French in + Picardy. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A LON']) + self.set_units(game, 'GERMANY', ['A HOL', 'A BEL', 'F HEL', 'F SKA']) + self.set_units(game, 'FRANCE', ['A PIC', 'A BUR']) + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - HOL', 'A LON - HOL']) + self.set_orders(game, 'GERMANY', ['A HOL S A BEL', 'A BEL S A HOL', 'F HEL S F SKA - NTH', 'F SKA - NTH']) + self.set_orders(game, 'FRANCE', ['A PIC - BEL', 'A BUR S A PIC - BEL']) + self.process(game) + assert self.check_results(game, 'F NTH', 'dislodged') + assert self.check_results(game, 'A LON', 'no convoy') + assert self.check_results(game, 'A HOL', '') + assert self.check_results(game, 'A BEL', 'cut') + assert self.check_results(game, 'F HEL', '') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'A PIC', 'bounce') + assert self.check_results(game, 'A BUR', '') + assert check_dislodged(game, 'F NTH', 'F SKA') + assert self.owner_name(game, 'F NTH') == 'GERMANY' + assert self.owner_name(game, 'A LON') == 'ENGLAND' + assert self.owner_name(game, 'A HOL') == 'GERMANY' + assert self.owner_name(game, 'A BEL') == 'GERMANY' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F SKA') is None + assert self.owner_name(game, 'A PIC') == 'FRANCE' + assert self.owner_name(game, 'A BUR') == 'FRANCE' + + def test_6_f_7(self): + """ 6.F.7. TEST CASE, DISLODGED CONVOY DOES NOT CAUSE CONTESTED AREA + When a fleet of a convoy is dislodged, the landing area is not contested, so other units can retreat to + that area. + England: F North Sea Convoys A London - Holland + England: A London - Holland + Germany: F Helgoland Bight Supports F Skagerrak - North Sea + Germany: F Skagerrak - North Sea + The dislodged English fleet can retreat to Holland. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A LON']) + self.set_units(game, 'GERMANY', ['F HEL', 'F SKA']) + + # Movements phase + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - HOL', 'A LON - HOL']) + self.set_orders(game, 'GERMANY', ['F HEL S F SKA - NTH', 'F SKA - NTH']) + self.process(game) + assert self.check_results(game, 'F NTH', 'dislodged') + assert self.check_results(game, 'A LON', 'no convoy') + assert self.check_results(game, 'F HEL', '') + assert self.check_results(game, 'F SKA', '') + assert check_dislodged(game, 'F NTH', 'F SKA') # ENGLAND + assert self.owner_name(game, 'F NTH') == 'GERMANY' + assert self.owner_name(game, 'A LON') == 'ENGLAND' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F SKA') is None + assert self.owner_name(game, 'A HOL') is None + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ENGLAND', 'F NTH R HOL') + self.process(game) + assert self.check_results(game, 'F NTH', '', phase='R') + assert self.owner_name(game, 'F NTH') == 'GERMANY' + assert self.owner_name(game, 'A LON') == 'ENGLAND' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F SKA') is None + assert self.owner_name(game, 'F HOL') == 'ENGLAND' + + def test_6_f_8(self): + """ 6.F.8. TEST CASE, DISLODGED CONVOY DOES NOT CAUSE A BOUNCE + When a fleet of a convoy is dislodged, then there will be no bounce in the landing area. + England: F North Sea Convoys A London - Holland + England: A London - Holland + Germany: F Helgoland Bight Supports F Skagerrak - North Sea + Germany: F Skagerrak - North Sea + Germany: A Belgium - Holland + The army in Belgium will not bounce and move to Holland. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A LON']) + self.set_units(game, 'GERMANY', ['F HEL', 'F SKA', 'A BEL']) + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - HOL', 'A LON - HOL']) + self.set_orders(game, 'GERMANY', ['F HEL S F SKA - NTH', 'F SKA - NTH', 'A BEL - HOL']) + self.process(game) + assert self.check_results(game, 'F NTH', 'dislodged') + assert self.check_results(game, 'A LON', 'no convoy') + assert self.check_results(game, 'F HEL', '') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'A BEL', '') + assert check_dislodged(game, 'F NTH', 'F SKA') + assert self.owner_name(game, 'F NTH') == 'GERMANY' + assert self.owner_name(game, 'A LON') == 'ENGLAND' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F SKA') is None + assert self.owner_name(game, 'A BEL') is None + assert self.owner_name(game, 'A HOL') == 'GERMANY' + + def test_6_f_9(self): + """ 6.F.9. TEST CASE, DISLODGE OF MULTI-ROUTE CONVOY + When a fleet of a convoy with multiple routes is dislodged, the result depends on the rulebook that is used. + England: F English Channel Convoys A London - Belgium + England: F North Sea Convoys A London - Belgium + England: A London - Belgium + France: F Brest Supports F Mid-Atlantic Ocean - English Channel + France: F Mid-Atlantic Ocean - English Channel + The French fleet in Mid Atlantic Ocean will dislodge the convoying fleet in the English Channel. If the + 1971 rules are used (see issue 4.A.1), this will disrupt the convoy and the army will stay in London. When + the 1982 or 2000 rulebook is used (which I prefer) the army can still go via the North Sea and the convoy + succeeds and the London army will end in Belgium. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F ENG', 'F NTH', 'A LON']) + self.set_units(game, 'FRANCE', ['F BRE', 'F MAO']) + self.set_orders(game, 'ENGLAND', ['F ENG C A LON - BEL', 'F NTH C A LON - BEL', 'A LON - BEL']) + self.set_orders(game, 'FRANCE', ['F BRE S F MAO - ENG', 'F MAO - ENG']) + self.process(game) + assert self.check_results(game, 'F ENG', 'dislodged') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'F BRE', '') + assert self.check_results(game, 'F MAO', '') + assert check_dislodged(game, 'F ENG', 'F MAO') + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A LON') is None + assert self.owner_name(game, 'F BRE') == 'FRANCE' + assert self.owner_name(game, 'F MAO') is None + assert self.owner_name(game, 'A BEL') == 'ENGLAND' + + def test_6_f_10(self): + """ 6.F.10. TEST CASE, DISLODGE OF MULTI-ROUTE CONVOY WITH FOREIGN FLEET + When the 1971 rulebook is used "unwanted" multi-route convoys are possible. + England: F North Sea Convoys A London - Belgium + England: A London - Belgium + Germany: F English Channel Convoys A London - Belgium + France: F Brest Supports F Mid-Atlantic Ocean - English Channel + France: F Mid-Atlantic Ocean - English Channel + If the 1982 or 2000 rulebook is used (which I prefer), it makes no difference that the convoying fleet in + the English Channel is German. It will take the convoy via the North Sea anyway and the army in London will + end in Belgium. However, when the 1971 rules are used, the German convoy is "unwanted". According to the + DPTG the German fleet should be ignored in the English convoy, since there is a convoy path with only + English fleets. That means that the convoy is not disrupted and the English army in London will end in + Belgium. See also issue 4.A.1. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A LON']) + self.set_units(game, 'GERMANY', 'F ENG') + self.set_units(game, 'FRANCE', ['F BRE', 'F MAO']) + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - BEL', 'A LON - BEL']) + self.set_orders(game, 'GERMANY', ['F ENG C A LON - BEL']) + self.set_orders(game, 'FRANCE', ['F BRE S F MAO - ENG', 'F MAO - ENG']) + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'F ENG', 'dislodged') + assert self.check_results(game, 'F BRE', '') + assert self.check_results(game, 'F MAO', '') + assert check_dislodged(game, 'F ENG', 'F MAO') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A LON') is None + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F BRE') == 'FRANCE' + assert self.owner_name(game, 'F MAO') is None + assert self.owner_name(game, 'A BEL') == 'ENGLAND' + + def test_6_f_11(self): + """ 6.F.11. TEST CASE, DISLODGE OF MULTI-ROUTE CONVOY WITH ONLY FOREIGN FLEETS + When the 1971 rulebook is used, "unwanted" convoys can not be ignored in all cases. + England: A London - Belgium + Germany: F English Channel Convoys A London - Belgium + Russia: F North Sea Convoys A London - Belgium + France: F Brest Supports F Mid-Atlantic Ocean - English Channel + France: F Mid-Atlantic Ocean - English Channel + If the 1982 or 2000 rulebook is used (which I prefer), it makes no difference that the convoying fleets + are not English. It will take the convoy via the North Sea anyway and the army in London will end in + Belgium. However, when the 1971 rules are used, the situation is different. Since both the fleet in the + English Channel as the fleet in North Sea are not English, it can not be concluded that the German fleet + is "unwanted". Therefore, one of the routes of the convoy is disrupted and that means that the complete + convoy is disrupted. The army in London will stay in London. See also issue 4.A.1. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A LON']) + self.set_units(game, 'GERMANY', ['F ENG']) + self.set_units(game, 'RUSSIA', ['F NTH']) + self.set_units(game, 'FRANCE', ['F BRE', 'F MAO']) + self.set_orders(game, 'ENGLAND', 'A LON - BEL') + self.set_orders(game, 'GERMANY', 'F ENG C A LON - BEL') + self.set_orders(game, 'RUSSIA', 'F NTH C A LON - BEL') + self.set_orders(game, 'FRANCE', ['F BRE S F MAO - ENG', 'F MAO - ENG']) + self.process(game) + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'F ENG', 'dislodged') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'F BRE', '') + assert self.check_results(game, 'F MAO', '') + assert check_dislodged(game, 'F ENG', 'F MAO') + assert self.owner_name(game, 'A LON') is None + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'RUSSIA' + assert self.owner_name(game, 'F BRE') == 'FRANCE' + assert self.owner_name(game, 'F MAO') is None + assert self.owner_name(game, 'A BEL') == 'ENGLAND' + + def test_6_f_12(self): + """ 6.F.12. TEST CASE, DISLODGED CONVOYING FLEET NOT ON ROUTE + When the rule is used that convoys are disrupted when one of the routes is disrupted (see issue 4.A.1), + the convoy is not necessarily disrupted when one of the fleets ordered to convoy is dislodged. + England: F English Channel Convoys A London - Belgium + England: A London - Belgium + England: F Irish Sea Convoys A London - Belgium + France: F North Atlantic Ocean Supports F Mid-Atlantic Ocean - Irish Sea + France: F Mid-Atlantic Ocean - Irish Sea + Even when convoys are disrupted when one of the routes is disrupted (see issue 4.A.1), the convoy from + London to Belgium will still succeed, since the dislodged fleet in the Irish Sea is not part of any route, + although it can be reached from the starting point London. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F ENG', 'A LON', 'F IRI']) + self.set_units(game, 'FRANCE', ['F NAO', 'F MAO']) + self.set_orders(game, 'ENGLAND', ['F ENG C A LON - BEL', 'A LON - BEL', 'F IRI C A LON - BEL']) + self.set_orders(game, 'FRANCE', ['F NAO S F MAO - IRI', 'F MAO - IRI']) + self.process(game) + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'F IRI', 'dislodged') + assert self.check_results(game, 'F NAO', '') + assert self.check_results(game, 'F MAO', '') + assert check_dislodged(game, 'F IRI', 'F MAO') + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + assert self.owner_name(game, 'A LON') is None + assert self.owner_name(game, 'F IRI') == 'FRANCE' + assert self.owner_name(game, 'F NAO') == 'FRANCE' + assert self.owner_name(game, 'F MAO') is None + assert self.owner_name(game, 'A BEL') == 'ENGLAND' + + def test_6_f_13(self): + """ 6.F.13. TEST CASE, THE UNWANTED ALTERNATIVE + This situation is not difficult to adjudicate, but it shows that even if someone wants to convoy, the + player might not want an alternative route for the convoy. + England: A London - Belgium + England: F North Sea Convoys A London - Belgium + France: F English Channel Convoys A London - Belgium + Germany: F Holland Supports F Denmark - North Sea + Germany: F Denmark - North Sea + If France and German are allies, England want to keep its army in London, to defend the island. An army + in Belgium could easily be destroyed by an alliance of France and Germany. England tries to be friends with + Germany, however France and Germany trick England. + The convoy of the army in London succeeds and the fleet in Denmark dislodges the fleet in the North Sea. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A LON', 'F NTH']) + self.set_units(game, 'FRANCE', 'F ENG') + self.set_units(game, 'GERMANY', ['F HOL', 'F DEN']) + self.set_orders(game, 'ENGLAND', ['A LON - BEL', 'F NTH C A LON - BEL']) + self.set_orders(game, 'FRANCE', 'F ENG C A LON - BEL') + self.set_orders(game, 'GERMANY', ['F HOL S F DEN - NTH', 'F DEN - NTH']) + self.process(game) + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'F NTH', 'dislodged') + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'F HOL', '') + assert self.check_results(game, 'F DEN', '') + assert check_dislodged(game, 'F NTH', 'F DEN') + assert self.owner_name(game, 'A LON') is None + assert self.owner_name(game, 'F NTH') == 'GERMANY' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F HOL') == 'GERMANY' + assert self.owner_name(game, 'F DEN') is None + assert self.owner_name(game, 'A BEL') == 'ENGLAND' + + def test_6_f_14(self): + """ 6.F.14. TEST CASE, SIMPLE CONVOY PARADOX + The most common paradox is when the attacked unit supports an attack on one of the convoying fleets. + England: F London Supports F Wales - English Channel + England: F Wales - English Channel + France: A Brest - London + France: F English Channel Convoys A Brest - London + This situation depends on how paradoxes are handled (see issue (4.A.2). In case of the 'All Hold' rule + (fully applied, not just as "backup" rule), both the movement of the English fleet in Wales as the France + convoy in Brest are part of the paradox and fail. In all other rules of paradoxical convoys (including the + Szykman rule which I prefer), the support of London is not cut. That means that the fleet in the English + Channel is dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F LON', 'F WAL']) + self.set_units(game, 'FRANCE', ['A BRE', 'F ENG']) + self.set_orders(game, 'ENGLAND', ['F LON S F WAL - ENG', 'F WAL - ENG']) + self.set_orders(game, 'FRANCE', ['A BRE - LON', 'F ENG C A BRE - LON']) + self.process(game) + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'F WAL', '') + assert self.check_results(game, 'A BRE', 'no convoy') + assert self.check_results(game, 'F ENG', 'dislodged') + assert check_dislodged(game, 'F ENG', 'F WAL') + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'F WAL') is None + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + + def test_6_f_15(self): + """ 6.F.15. TEST CASE, SIMPLE CONVOY PARADOX WITH ADDITIONAL CONVOY + Paradox rules only apply on the paradox core. + England: F London Supports F Wales - English Channel + England: F Wales - English Channel + France: A Brest - London + France: F English Channel Convoys A Brest - London + Italy: F Irish Sea Convoys A North Africa - Wales + Italy: F Mid-Atlantic Ocean Convoys A North Africa - Wales + Italy: A North Africa - Wales + The Italian convoy is not part of the paradox core and should therefore succeed when the move of the + fleet in Wales is successful. This is the case except when the 'All Hold' paradox rule is used (fully + applied, not just as "backup" rule, see issue 4.A.2). + I prefer the Szykman rule, so I prefer that both the fleet in Wales as the army in North Africa succeed in + moving. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F LON', 'F WAL']) + self.set_units(game, 'FRANCE', ['A BRE', 'F ENG']) + self.set_units(game, 'ITALY', ['F IRI', 'F MAO', 'A NAF']) + self.set_orders(game, 'ENGLAND', ['F LON S F WAL - ENG', 'F WAL - ENG']) + self.set_orders(game, 'FRANCE', ['A BRE - LON', 'F ENG C A BRE - LON']) + self.set_orders(game, 'ITALY', ['F IRI C A NAF - WAL', 'F MAO C A NAF - WAL', 'A NAF - WAL']) + self.process(game) + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'F WAL', '') + assert self.check_results(game, 'A BRE', 'no convoy') + assert self.check_results(game, 'F ENG', 'dislodged') + assert self.check_results(game, 'F IRI', '') + assert self.check_results(game, 'F MAO', '') + assert self.check_results(game, 'A NAF', '') + assert check_dislodged(game, 'F ENG', 'F WAL') + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'A WAL') == 'ITALY' + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + assert self.owner_name(game, 'F IRI') == 'ITALY' + assert self.owner_name(game, 'F MAO') == 'ITALY' + assert self.owner_name(game, 'A NAF') is None + + def test_6_f_16(self): + """ 6.F.16. TEST CASE, PANDIN'S PARADOX + In Pandin's paradox, the attacked unit protects the convoying fleet by a beleaguered garrison. + England: F London Supports F Wales - English Channel + England: F Wales - English Channel + France: A Brest - London + France: F English Channel Convoys A Brest - London + Germany: F North Sea Supports F Belgium - English Channel + Germany: F Belgium - English Channel + In all the different rules for resolving convoy disruption paradoxes (see issue 4.A.2), the support + of London is not cut. That means that the fleet in the English Channel is not dislodged and none of the + units succeed to move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F LON', 'F WAL']) + self.set_units(game, 'FRANCE', ['A BRE', 'F ENG']) + self.set_units(game, 'GERMANY', ['F NTH', 'F BEL']) + self.set_orders(game, 'ENGLAND', ['F LON S F WAL - ENG', 'F WAL - ENG']) + self.set_orders(game, 'FRANCE', ['A BRE - LON', 'F ENG C A BRE - LON']) + self.set_orders(game, 'GERMANY', ['F NTH S F BEL - ENG', 'F BEL - ENG']) + self.process(game) + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'F WAL', 'bounce') + assert self.check_results(game, 'A BRE', 'no convoy') + assert self.check_results(game, 'F ENG', 'disrupted') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'F BEL', 'bounce') + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'F WAL') == 'ENGLAND' + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'GERMANY' + assert self.owner_name(game, 'F BEL') == 'GERMANY' + + def test_6_f_17(self): + """ 6.F.17. TEST CASE, PANDIN'S EXTENDED PARADOX + In Pandin's extended paradox, the attacked unit protects the convoying fleet by a beleaguered garrison and + the attacked unit can dislodge the unit that gives the protection. + England: F London Supports F Wales - English Channel + England: F Wales - English Channel + France: A Brest - London + France: F English Channel Convoys A Brest - London + France: F Yorkshire Supports A Brest - London + Germany: F North Sea Supports F Belgium - English Channel + Germany: F Belgium - English Channel + When the 1971, 1982 or 2000 rule is used (see issue 4.A.2), the support of London is not cut. That means + that the fleet in the English Channel is not dislodged. The convoy will succeed and dislodge the fleet in + London. You may argue that this violates the dislodge rule, but the common interpretation is that the + paradox convoy rules take precedence over the dislodge rule. + If the Simon Szykman alternative is used (which I prefer), the convoy fails and the fleet in London and + the English Channel are not dislodged. When the 'All Hold' (fully applied, not just as "backup" rule) or + the DPTG rule is used, the result is the same as the Simon Szykman alternative. The involved moves (the + move of the German fleet in Belgium and the convoying army in Brest) fail. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F LON', 'F WAL']) + self.set_units(game, 'FRANCE', ['A BRE', 'F ENG', 'F YOR']) + self.set_units(game, 'GERMANY', ['F NTH', 'F BEL']) + self.set_orders(game, 'ENGLAND', ['F LON S F WAL - ENG', 'F WAL - ENG']) + self.set_orders(game, 'FRANCE', ['A BRE - LON', 'F ENG C A BRE - LON', 'F YOR S A BRE - LON']) + self.set_orders(game, 'GERMANY', ['F NTH S F BEL - ENG', 'F BEL - ENG']) + self.process(game) + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'F WAL', 'bounce') + assert self.check_results(game, 'A BRE', 'no convoy') + assert self.check_results(game, 'F ENG', 'disrupted') + assert self.check_results(game, 'F YOR', 'no convoy') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'F BEL', 'bounce') + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'F WAL') == 'ENGLAND' + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F YOR') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'GERMANY' + assert self.owner_name(game, 'F BEL') == 'GERMANY' + + def test_6_f_18(self): + """ 6.F.18. TEST CASE, BETRAYAL PARADOX + The betrayal paradox is comparable to Pandin's paradox, but now the attacked unit direct supports the + convoying fleet. Of course, this will only happen when the player of the attacked unit is betrayed. + England: F North Sea Convoys A London - Belgium + England: A London - Belgium + England: F English Channel Supports A London - Belgium + France: F Belgium Supports F North Sea + Germany: F Helgoland Bight Supports F Skagerrak - North Sea + Germany: F Skagerrak - North Sea + If the English convoy from London to Belgium is successful, then it cuts the France support necessary to + hold the fleet in the North Sea (see issue 4.A.2). + The 1971 and 2000 ruling do not give an answer on this. + According to the 1982 ruling the French support on the North Sea will not be cut. So, the fleet in the + North Sea will not be dislodged by the Germans and the army in London will dislodge the French army in + Belgium. + If the Szykman rule is followed (which I prefer), the move of the army in London will fail and will not cut + support. That means that the fleet in the North Sea will not be dislodged. The 'All Hold' rule has the same + result as the Szykman rule, but with a different reason. The move of the army in London and the move of the + German fleet in Skagerrak will fail. Since a failing convoy does not result in a consistent resolution, + the DPTG gives the same result as the 'All Hold' rule. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A LON', 'F ENG']) + self.set_units(game, 'FRANCE', ['F BEL']) + self.set_units(game, 'GERMANY', ['F HEL', 'F SKA']) + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - BEL', 'A LON - BEL', 'F ENG S A LON - BEL']) + self.set_orders(game, 'FRANCE', ['F BEL S F NTH']) + self.set_orders(game, 'GERMANY', ['F HEL S F SKA - NTH', 'F SKA - NTH']) + self.process(game) + assert self.check_results(game, 'F NTH', 'disrupted') + assert self.check_results(game, 'A LON', 'no convoy') + assert self.check_results(game, 'F ENG', 'no convoy') + assert self.check_results(game, 'F BEL', '') + assert self.check_results(game, 'F HEL', '') + assert self.check_results(game, 'F SKA', 'bounce') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A LON') == 'ENGLAND' + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + assert self.owner_name(game, 'F BEL') == 'FRANCE' + assert self.owner_name(game, 'F HEL') == 'GERMANY' + assert self.owner_name(game, 'F SKA') == 'GERMANY' + + def test_6_f_19(self): + """ 6.F.19. TEST CASE, MULTI-ROUTE CONVOY DISRUPTION PARADOX + The situation becomes more complex when the convoy has alternative routes. + France: A Tunis - Naples + France: F Tyrrhenian Sea Convoys A Tunis - Naples + France: F Ionian Sea Convoys A Tunis - Naples + Italy: F Naples Supports F Rome - Tyrrhenian Sea + Italy: F Rome - Tyrrhenian Sea + Now, two issues play a role. The ruling about disruption of convoys (issue 4.A.1) and the issue how + paradoxes are resolved (issue 4.A.2). + If the 1971 rule is used about multi-route convoys (when one of the routes is disrupted, the convoy fails), + this test case is just a simple paradox. For the 1971, 1982, 2000 and Szykman paradox rule, the support of + the fleet in Naples is not cut and the fleet in Rome dislodges the fleet in the Tyrrhenian Sea. When the + 'All Hold' rule is used, both the convoy of the army in Tunis as the move of the fleet in Rome will fail. + When the 1982 rule is used about multi-route convoy disruption, then convoys are disrupted when all routes + are disrupted (this is the rule I prefer). With this rule, the situation becomes paradoxical. According to + the 1971 and 1982 paradox rules, the support given by the fleet in Naples is not cut, that means that the + fleet in the Tyrrhenian Sea is dislodged. + According to the 2000 ruling the fleet in the Tyrrhenian Sea is not "necessary" for the convoy and the + support of Naples is cut and the fleet in the Tyrrhenian Sea is not dislodged. + If the Szykman rule is used (which I prefer), the 'All Hold' rule or the DPTG, then there is no paradoxical + situation. The support of Naples is cut and the fleet in the Tyrrhenian Sea is not dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['A TUN', 'F TYS', 'F ION']) + self.set_units(game, 'ITALY', ['F NAP', 'F ROM']) + self.set_orders(game, 'FRANCE', ['A TUN - NAP', 'F TYS C A TUN - NAP', 'F ION C A TUN - NAP']) + self.set_orders(game, 'ITALY', ['F NAP S F ROM - TYS', 'F ROM - TYS']) + self.process(game) + assert self.check_results(game, 'A TUN', 'bounce') + assert self.check_results(game, 'F TYS', '') + assert self.check_results(game, 'F ION', '') + assert self.check_results(game, 'F NAP', 'cut') + assert self.check_results(game, 'F ROM', 'bounce') + assert self.owner_name(game, 'A TUN') == 'FRANCE' + assert self.owner_name(game, 'F TYS') == 'FRANCE' + assert self.owner_name(game, 'F ION') == 'FRANCE' + assert self.owner_name(game, 'F NAP') == 'ITALY' + assert self.owner_name(game, 'F ROM') == 'ITALY' + + def test_6_f_20(self): + """ 6.F.20. TEST CASE, UNWANTED MULTI-ROUTE CONVOY PARADOX + The 1982 paradox rule allows some creative defense. + France: A Tunis - Naples + France: F Tyrrhenian Sea Convoys A Tunis - Naples + Italy: F Naples Supports F Ionian Sea + Italy: F Ionian Sea Convoys A Tunis - Naples + Turkey: F Aegean Sea Supports F Eastern Mediterranean - Ionian Sea + Turkey: F Eastern Mediterranean - Ionian Sea + Again, two issues play a role. The ruling about disruption of multi-route convoys (issue 4.A.1) and the + issue how paradoxes are resolved (issue 4.A.2). + If the 1971 rule is used about multi-route convoys (when one of the routes is disrupted, the convoy fails), + the Italian convoy order in the Ionian Sea is not part of the convoy, because it is a foreign unit + (according to the DPTG). + That means that the fleet in the Ionian Sea is not a 'convoying' fleet. In all rulings the support of + Naples on the Ionian Sea is cut and the fleet in the Ionian Sea is dislodged by the Turkish fleet in the + Eastern Mediterranean. When the 1982 rule is used about multi-route convoy disruption, then convoys are + disrupted when all routes are disrupted (this is the rule I prefer). With this rule, the situation becomes + paradoxical. According to the 1971 and 1982 paradox rules, the support given by the fleet in Naples is not + cut, that means that the fleet in the Ionian Sea is not dislodged. + According to the 2000 ruling the fleet in the Ionian Sea is not "necessary" and the support of Naples is + cut and the fleet in the Ionian Sea is dislodged by the Turkish fleet in the Eastern Mediterranean. + If the Szykman rule, the 'All Hold' rule or DPTG is used, then there is no paradoxical situation. The + support of Naples is cut and the fleet in the Ionian Sea is dislodged by the Turkish fleet in the Eastern + Mediterranean. As you can see, the 1982 rules allows the Italian player to save its fleet in the Ionian Sea + with a trick. I do not consider this trick as normal tactical play. I prefer the Szykman rule as one of the + rules that does not allow this trick. According to this rule the fleet in the Ionian Sea is dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['A TUN', 'F TYS']) + self.set_units(game, 'ITALY', ['F NAP', 'F ION']) + self.set_units(game, 'TURKEY', ['F AEG', 'F EAS']) + self.set_orders(game, 'FRANCE', ['A TUN - NAP', 'F TYS C A TUN - NAP']) + self.set_orders(game, 'ITALY', ['F NAP S F ION', 'F ION C A TUN - NAP']) + self.set_orders(game, 'TURKEY', ['F AEG S F EAS - ION', 'F EAS - ION']) + self.process(game) + assert self.check_results(game, 'A TUN', 'bounce') + assert self.check_results(game, 'F TYS', '') + assert self.check_results(game, 'F NAP', 'cut') + assert self.check_results(game, 'F ION', 'dislodged') + assert self.check_results(game, 'F AEG', '') + assert self.check_results(game, 'F EAS', '') + assert check_dislodged(game, 'F ION', 'F EAS') + assert self.owner_name(game, 'A TUN') == 'FRANCE' + assert self.owner_name(game, 'F TYS') == 'FRANCE' + assert self.owner_name(game, 'F NAP') == 'ITALY' + assert self.owner_name(game, 'F ION') == 'TURKEY' + assert self.owner_name(game, 'F AEG') == 'TURKEY' + assert self.owner_name(game, 'F EAS') is None + + def test_6_f_21(self): + """ 6.F.21. TEST CASE, DAD'S ARMY CONVOY + The 1982 paradox rule has as side effect that convoying armies do not cut support in some situations that + are not paradoxical. + Russia: A Edinburgh Supports A Norway - Clyde + Russia: F Norwegian Sea Convoys A Norway - Clyde + Russia: A Norway - Clyde + France: F Irish Sea Supports F Mid-Atlantic Ocean - North Atlantic Ocean + France: F Mid-Atlantic Ocean - North Atlantic Ocean + England: A Liverpool - Clyde via Convoy + England: F North Atlantic Ocean Convoys A Liverpool - Clyde + England: F Clyde Supports F North Atlantic Ocean + In all rulings, except the 1982 paradox ruling, the support of the fleet in Clyde on the North Atlantic + Ocean is cut and the French fleet in the Mid-Atlantic Ocean will dislodge the fleet in the North Atlantic + Ocean. This is the preferred way. However, in the 1982 paradox rule (see issue 4.A.2), the support of the + fleet in Clyde is not cut. That means that the English fleet in the North Atlantic Ocean is not dislodged. + As you can see, the 1982 rule allows England to save its fleet in the North Atlantic Ocean in a very + strange way. Just the support of Clyde is insufficient (if there is no convoy, the support is cut). Only + the convoy to the area occupied by own unit, can do the trick in this situation. The embarking of troops + in the fleet deceives the enemy so much that it works as a magic cloak. The enemy is not able to dislodge + the fleet in the North Atlantic Ocean any more. Of course, this will only work in comedies. I prefer the + Szykman rule as one of the rules that does not allow this trick. According to this rule (and all other + paradox rules), the fleet in the North Atlantic is just dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'RUSSIA', ['A EDI', 'F NWG', 'A NWY']) + self.set_units(game, 'FRANCE', ['F IRI', 'F MAO']) + self.set_units(game, 'ENGLAND', ['A LVP', 'F NAO', 'F CLY']) + self.set_orders(game, 'RUSSIA', ['A EDI S A NWY - CLY', 'F NWG C A NWY - CLY', 'A NWY - CLY']) + self.set_orders(game, 'FRANCE', ['F IRI S F MAO - NAO', 'F MAO - NAO']) + self.set_orders(game, 'ENGLAND', ['A LVP - CLY VIA', 'F NAO C A LVP - CLY', 'F CLY S F NAO']) + self.process(game) + assert self.check_results(game, 'A EDI', '') + assert self.check_results(game, 'F NWG', '') + assert self.check_results(game, 'A NWY', '') + assert self.check_results(game, 'F IRI', '') + assert self.check_results(game, 'F MAO', '') + assert self.check_results(game, 'A LVP', 'no convoy') + assert self.check_results(game, 'F NAO', 'dislodged') + assert self.check_results(game, 'F CLY', 'cut') + assert self.check_results(game, 'F CLY', 'dislodged') + assert check_dislodged(game, 'F NAO', 'F MAO') + assert check_dislodged(game, 'F CLY', 'A NWY') + assert self.owner_name(game, 'A EDI') == 'RUSSIA' + assert self.owner_name(game, 'F NWG') == 'RUSSIA' + assert self.owner_name(game, 'A NWY') is None + assert self.owner_name(game, 'F IRI') == 'FRANCE' + assert self.owner_name(game, 'F MAO') is None + assert self.owner_name(game, 'A LVP') == 'ENGLAND' + assert self.owner_name(game, 'F NAO') == 'FRANCE' + assert self.owner_name(game, 'A CLY') == 'RUSSIA' + + def test_6_f_22(self): + """ 6.F.22. TEST CASE, SECOND ORDER PARADOX WITH TWO RESOLUTIONS + Two convoys are involved in a second order paradox. + England: F Edinburgh - North Sea + England: F London Supports F Edinburgh - North Sea + France: A Brest - London + France: F English Channel Convoys A Brest - London + Germany: F Belgium Supports F Picardy - English Channel + Germany: F Picardy - English Channel + Russia: A Norway - Belgium + Russia: F North Sea Convoys A Norway - Belgium + Without any paradox rule, there are two consistent resolutions. The supports of the English fleet in London + and the German fleet in Picardy are not cut. That means that the French fleet in the English Channel and + the Russian fleet in the North Sea are dislodged, which makes it impossible to cut the support. The other + resolution is that the supports of the English fleet in London the German fleet in Picardy are cut. In that + case the French fleet in the English Channel and the Russian fleet in the North Sea will survive and will + not be dislodged. This gives the possibility to cut the support. + The 1971 paradox rule and the 2000 rule (see issue 4.A.2) do not have an answer on this. + According to the 1982 rule, the supports are not cut which means that the French fleet in the English + Channel and the Russian fleet in the North Sea are dislodged. + The Szykman (which I prefer), has the same result as the 1982 rule. The supports are not cut, the convoying + armies fail to move, the fleet in Picardy dislodges the fleet in English Channel and the fleet in Edinburgh + dislodges the fleet in the North Sea. + The DPTG rule has in this case the same result as the Szykman rule, because the failing of all convoys is a + consistent resolution. So, the armies in Brest and Norway fail to move, while the fleets in Edinburgh and + Picardy succeed to move. When the 'All Hold' rule is used, the movement of the armies in Brest and Norway + as the fleets in Edinburgh and Picardy will fail. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F EDI', 'F LON']) + self.set_units(game, 'FRANCE', ['A BRE', 'F ENG']) + self.set_units(game, 'GERMANY', ['F BEL', 'F PIC']) + self.set_units(game, 'RUSSIA', ['A NWY', 'F NTH']) + self.set_orders(game, 'ENGLAND', ['F EDI - NTH', 'F LON S F EDI - NTH']) + self.set_orders(game, 'FRANCE', ['A BRE - LON', 'F ENG C A BRE - LON']) + self.set_orders(game, 'GERMANY', ['F BEL S F PIC - ENG', 'F PIC - ENG']) + self.set_orders(game, 'RUSSIA', ['A NWY - BEL', 'F NTH C A NWY - BEL']) + self.process(game) + assert self.check_results(game, 'F EDI', '') + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'A BRE', 'no convoy') + assert self.check_results(game, 'F ENG', 'dislodged') + assert self.check_results(game, 'F BEL', '') + assert self.check_results(game, 'F PIC', '') + assert self.check_results(game, 'A NWY', 'no convoy') + assert self.check_results(game, 'F NTH', 'dislodged') + assert check_dislodged(game, 'F ENG', 'F PIC') + assert check_dislodged(game, 'F NTH', 'F EDI') + assert self.owner_name(game, 'F EDI') is None + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'GERMANY' + assert self.owner_name(game, 'F BEL') == 'GERMANY' + assert self.owner_name(game, 'F PIC') is None + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + + def test_6_f_23(self): + """ 6.F.23. TEST CASE, SECOND ORDER PARADOX WITH TWO EXCLUSIVE CONVOYS + In this paradox there are two consistent resolutions, but where the two convoys do not fail or succeed at + the same time. This fact is important for the DPTG resolution. + England: F Edinburgh - North Sea + England: F Yorkshire Supports F Edinburgh - North Sea + France: A Brest - London + France: F English Channel Convoys A Brest - London + Germany: F Belgium Supports F English Channel + Germany: F London Supports F North Sea + Italy: F Mid-Atlantic Ocean - English Channel + Italy: F Irish Sea Supports F Mid-Atlantic Ocean - English Channel + Russia: A Norway - Belgium + Russia: F North Sea Convoys A Norway - Belgium + Without any paradox rule, there are two consistent resolutions. In one resolution, the convoy in the + English Channel is dislodged by the fleet in the Mid-Atlantic Ocean, while the convoy in the North Sea + succeeds. In the other resolution, it is the other way around. The convoy in the North Sea is dislodged by + the fleet in Edinburgh, while the convoy in the English Channel succeeds. + The 1971 paradox rule and the 2000 rule (see issue 4.A.2) do not have an answer on this. + According to the 1982 rule, the supports are not cut which means that the none of the units move. + The Szykman (which I prefer), has the same result as the 1982 rule. The convoying armies fail to move and + the supports are not cut. Because of the failure to cut the support, no fleet succeeds to move. + When the 'All Hold' rule is used, the movement of the armies and the fleets all fail. + Since there is no consistent resolution where all convoys fail, the DPTG rule has the same result as the + 'All Hold' rule. That means the movement of all units fail. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F EDI', 'F YOR']) + self.set_units(game, 'FRANCE', ['A BRE', 'F ENG']) + self.set_units(game, 'GERMANY', ['F BEL', 'F LON']) + self.set_units(game, 'ITALY', ['F MAO', 'F IRI']) + self.set_units(game, 'RUSSIA', ['A NWY', 'F NTH']) + self.set_orders(game, 'ENGLAND', ['F EDI - NTH', 'F YOR S F EDI - NTH']) + self.set_orders(game, 'FRANCE', ['A BRE - LON', 'F ENG C A BRE - LON']) + self.set_orders(game, 'GERMANY', ['F BEL S F ENG', 'F LON S F NTH']) + self.set_orders(game, 'ITALY', ['F MAO - ENG', 'F IRI S F MAO - ENG']) + self.set_orders(game, 'RUSSIA', ['A NWY - BEL', 'F NTH C A NWY - BEL']) + self.process(game) + assert self.check_results(game, 'F EDI', 'bounce') + assert self.check_results(game, 'F YOR', '') + assert self.check_results(game, 'A BRE', 'no convoy') + assert self.check_results(game, 'F ENG', 'disrupted') + assert self.check_results(game, 'F BEL', '') + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'F MAO', 'bounce') + assert self.check_results(game, 'F IRI', '') + assert self.check_results(game, 'A NWY', 'no convoy') + assert self.check_results(game, 'F NTH', 'disrupted') + assert self.owner_name(game, 'F EDI') == 'ENGLAND' + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F BEL') == 'GERMANY' + assert self.owner_name(game, 'F LON') == 'GERMANY' + assert self.owner_name(game, 'F MAO') == 'ITALY' + assert self.owner_name(game, 'F IRI') == 'ITALY' + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'F NTH') == 'RUSSIA' + + def test_6_f_24(self): + """ 6.F.24. TEST CASE, SECOND ORDER PARADOX WITH NO RESOLUTION + As first order paradoxes, second order paradoxes come in two flavors, with two resolutions or no resolution. + England: F Edinburgh - North Sea + England: F London Supports F Edinburgh - North Sea + England: F Irish Sea - English Channel + England: F Mid-Atlantic Ocean Supports F Irish Sea - English Channel + France: A Brest - London + France: F English Channel Convoys A Brest - London + France: F Belgium Supports F English Channel + Russia: A Norway - Belgium + Russia: F North Sea Convoys A Norway - Belgium + When no paradox rule is used, there is no consistent resolution. If the French support in Belgium is cut, + the French fleet in the English Channel will be dislodged. That means that the support of London will not + be cut and the fleet in Edinburgh will dislodge the Russian fleet in the North Sea. In this way the support + in Belgium is not cut! But if the support in Belgium is not cut, the Russian fleet in the North Sea will + not be dislodged and the army in Norway can cut the support in Belgium. + The 1971 paradox rule and the 2000 rule (see issue 4.A.2) do not have an answer on this. According to the + 1982 rule, the supports are not cut which means that the French fleet in the English Channel will survive + and but the Russian fleet in the North Sea is dislodged. + If the Szykman alternative is used (which I prefer), the supports are not cut and the convoying armies fail + to move, which has the same result as the 1982 rule in this case. + When the 'All Hold' rule is used, the movement of the armies in Brest and Norway as the fleets in Edinburgh + and the Irish Sea will fail. Since there is no consistent resolution where all convoys fail, the DPTG has + in this case the same result as the 'All Hold' rule. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F EDI', 'F LON', 'F IRI', 'F MAO']) + self.set_units(game, 'FRANCE', ['A BRE', 'F ENG', 'F BEL']) + self.set_units(game, 'RUSSIA', ['A NWY', 'F NTH']) + self.set_orders(game, 'ENGLAND', ['F EDI - NTH', 'F LON S F EDI - NTH', 'F IRI - ENG', 'F MAO S F IRI - ENG']) + self.set_orders(game, 'FRANCE', ['A BRE - LON', 'F ENG C A BRE - LON', 'F BEL S F ENG']) + self.set_orders(game, 'RUSSIA', ['A NWY - BEL', 'F NTH C A NWY - BEL']) + self.process(game) + assert self.check_results(game, 'F EDI', '') + assert self.check_results(game, 'F LON', '') + assert self.check_results(game, 'F IRI', 'bounce') + assert self.check_results(game, 'F MAO', '') + assert self.check_results(game, 'A BRE', 'no convoy') + assert self.check_results(game, 'F ENG', 'disrupted') + assert self.check_results(game, 'F BEL', '') + assert self.check_results(game, 'A NWY', 'no convoy') + assert self.check_results(game, 'F NTH', 'dislodged') + assert check_dislodged(game, 'F NTH', 'F EDI') + assert self.owner_name(game, 'F EDI') is None + assert self.owner_name(game, 'F LON') == 'ENGLAND' + assert self.owner_name(game, 'F IRI') == 'ENGLAND' + assert self.owner_name(game, 'F MAO') == 'ENGLAND' + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F BEL') == 'FRANCE' + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + + # 6.G. TEST CASES, CONVOYING TO ADJACENT PLACES + def test_6_g_1(self): + """ 6.G.1. TEST CASE, TWO UNITS CAN SWAP PLACES BY CONVOY + The only way to swap two units, is by convoy. + England: A Norway - Sweden + England: F Skagerrak Convoys A Norway - Sweden + Russia: A Sweden - Norway + In most interpretation of the rules, the units in Norway and Sweden will be swapped. However, if + explicit adjacent convoying is used (see issue 4.A.3), then it is just a head to head battle. + I prefer the 2000 rules, so the units are swapped. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A NWY', 'F SKA']) + self.set_units(game, 'RUSSIA', ['A SWE']) + self.set_orders(game, 'ENGLAND', ['A NWY - SWE', 'F SKA C A NWY - SWE']) + self.set_orders(game, 'RUSSIA', ['A SWE - NWY']) + self.process(game) + assert self.check_results(game, 'A NWY', '') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'A SWE', '') + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'F SKA') == 'ENGLAND' + assert self.owner_name(game, 'A SWE') == 'ENGLAND' + + def test_6_g_2(self): + """ 6.G.2. TEST CASE, KIDNAPPING AN ARMY + Germany promised England to support to dislodge the Russian fleet in Sweden and it promised Russia to + support to dislodge the English army in Norway. Instead, the joking German orders a convoy. + England: A Norway - Sweden + Russia: F Sweden - Norway + Germany: F Skagerrak Convoys A Norway - Sweden + See issue 4.A.3. + When the 1982/2000 rulebook is used (which I prefer), England has no intent to swap and it is just a head + to head battle were both units will fail to move. When explicit adjacent convoying is used (DPTG), the + English move is not a convoy and again it just a head to head battle were both units will fail to move. + In all other interpretations, the army in Norway will be convoyed and swap its place with the fleet in + Sweden. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', 'A NWY') + self.set_units(game, 'RUSSIA', 'F SWE') + self.set_units(game, 'GERMANY', 'F SKA') + self.set_orders(game, 'ENGLAND', 'A NWY - SWE') + self.set_orders(game, 'RUSSIA', 'F SWE - NWY') + self.set_orders(game, 'GERMANY', 'F SKA C A NWY - SWE') + self.process(game) + assert self.check_results(game, 'A NWY', 'bounce') + assert self.check_results(game, 'F SWE', 'bounce') + assert self.check_results(game, 'F SKA', 'no convoy') + assert self.owner_name(game, 'A NWY') == 'ENGLAND' + assert self.owner_name(game, 'F SWE') == 'RUSSIA' + assert self.owner_name(game, 'F SKA') == 'GERMANY' + + def test_6_g_3(self): + """ 6.G.3. TEST CASE, KIDNAPPING WITH A DISRUPTED CONVOY + When kidnapping of armies is allowed, a move can be sabotaged by a fleet that is almost certainly dislodged. + France: F Brest - English Channel + France: A Picardy - Belgium + France: A Burgundy Supports A Picardy - Belgium + France: F Mid-Atlantic Ocean Supports F Brest - English Channel + England: F English Channel Convoys A Picardy - Belgium + See issue 4.A.3. If a convoy always takes precedence over a land route (choice a), the move from Picardy to + Belgium fails. It tries to convoy and the convoy is disrupted. + For choice b and c, there is no unit moving in opposite direction for the move of the army in Picardy. + For this reason, the move for the army in Picardy is not by convoy and succeeds over land. + When the 1982 or 2000 rules are used (choice d), then it is not the "intent" of the French army in Picardy + to convoy. The move from Picardy to Belgium is just a successful move over land. + When explicit adjacent convoying is used (DPTG, choice e), the order of the French army in Picardy is not + a convoy order. So, it just ordered over land, and that move succeeds. This is an excellent example why + the convoy route should not automatically have priority over the land route. It would just be annoying for + the attacker and this situation is without fun. I prefer the 1982 rule with the 2000 clarification. + According to these rules the move from Picardy succeeds. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['F BRE', 'A PIC', 'A BUR', 'F MAO']) + self.set_units(game, 'ENGLAND', ['F ENG']) + self.set_orders(game, 'FRANCE', ['F BRE - ENG', 'A PIC - BEL', 'A BUR S A PIC - BEL', 'F MAO S F BRE - ENG']) + self.set_orders(game, 'ENGLAND', ['F ENG C A PIC - BEL']) + self.process(game) + assert self.check_results(game, 'F BRE', '') + assert self.check_results(game, 'A PIC', '') + assert self.check_results(game, 'A BUR', '') + assert self.check_results(game, 'F MAO', '') + assert self.check_results(game, 'F ENG', 'dislodged') + assert self.check_results(game, 'F ENG', 'no convoy') + assert check_dislodged(game, 'F ENG', 'F BRE') + assert self.owner_name(game, 'F BRE') is None + assert self.owner_name(game, 'A PIC') is None + assert self.owner_name(game, 'A BUR') == 'FRANCE' + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'A BEL') == 'FRANCE' + + def test_6_g_4(self): + """ 6.G.4. TEST CASE, KIDNAPPING WITH A DISRUPTED CONVOY AND OPPOSITE MOVE + In the situation of the previous test case it was rather clear that the army didn't want to take the + convoy. But what if there is an army moving in opposite direction? + France: F Brest - English Channel + France: A Picardy - Belgium + France: A Burgundy Supports A Picardy - Belgium + France: F Mid-Atlantic Ocean Supports F Brest - English Channel + England: F English Channel Convoys A Picardy - Belgium + England: A Belgium - Picardy + See issue 4.A.3. If a convoy always takes precedence over a land route (choice a), the move from Picardy to + Belgium fails. It tries to convoy and the convoy is disrupted. + For choice b the convoy is also taken, because there is a unit in Belgium moving in opposite direction. + This means that the convoy is disrupted and the move from Picardy to Belgium fails. + For choice c the convoy is not taken. Although, the unit in Belgium is moving in opposite direction, + the army will not take a disrupted convoy. So, the move from Picardy to Belgium succeeds. + When the 1982 or 2000 rules are used (choice d), then it is not the "intent" of the French army in Picardy + to convoy. The move from Picardy to Belgium is just a successful move over land. + When explicit adjacent convoying is used (DPTG, choice e), the order of the French army in Picardy is not + a convoy order. So, it just ordered over land, and that move succeeds. + Again an excellent example why the convoy route should not automatically have priority over the land route. + It would just be annoying for the attacker and this situation is without fun. I prefer the 1982 rule with + the 2000 clarification. According to these rules the move from Picardy succeeds. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['F BRE', 'A PIC', 'A BUR', 'F MAO']) + self.set_units(game, 'ENGLAND', ['F ENG', 'A BEL']) + self.set_orders(game, 'FRANCE', ['F BRE - ENG', 'A PIC - BEL', 'A BUR S A PIC - BEL', 'F MAO S F BRE - ENG']) + self.set_orders(game, 'ENGLAND', ['F ENG C A PIC - BEL', 'A BEL - PIC']) + self.process(game) + assert self.check_results(game, 'F BRE', '') + assert self.check_results(game, 'A PIC', '') + assert self.check_results(game, 'A BUR', '') + assert self.check_results(game, 'F MAO', '') + assert self.check_results(game, 'F ENG', 'dislodged') + assert self.check_results(game, 'F ENG', 'no convoy') + assert self.check_results(game, 'A BEL', 'dislodged') + assert check_dislodged(game, 'F ENG', 'F BRE') + assert check_dislodged(game, 'A BEL', 'A PIC') + assert self.owner_name(game, 'F BRE') is None + assert self.owner_name(game, 'A PIC') is None + assert self.owner_name(game, 'A BUR') == 'FRANCE' + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'A BEL') == 'FRANCE' + + def test_6_g_5(self): + """ 6.G.5. TEST CASE, SWAPPING WITH INTENT + When one of the convoying fleets is of the same nationality of the convoyed army, the "intent" is to convoy. + Italy: A Rome - Apulia + Italy: F Tyrrhenian Sea Convoys A Apulia - Rome + Turkey: A Apulia - Rome + Turkey: F Ionian Sea Convoys A Apulia - Rome + See issue 4.A.3. When the 1982/2000 rulebook is used (which I prefer), the convoy depends on the "intent". + Since there is an own fleet in the convoy, the intent is to convoy and the armies in Rome and Apulia swap + places. For choices a, b and c of the issue there is also a convoy and the same swap takes place. + When explicit adjacent convoying is used (DPTG, choice e), then the Turkish army did not receive an order + to move by convoy. So, it is just a head to head battle and both the army in Rome and Apulia will not move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ITALY', ['A ROM', 'F TYS']) + self.set_units(game, 'TURKEY', ['A APU', 'F ION']) + self.set_orders(game, 'ITALY', ['A ROM - APU', 'F TYS C A APU - ROM']) + self.set_orders(game, 'TURKEY', ['A APU - ROM', 'F ION C A APU - ROM']) + self.process(game) + assert self.check_results(game, 'A ROM', '') + assert self.check_results(game, 'F TYS', '') + assert self.check_results(game, 'A APU', '') + assert self.check_results(game, 'F ION', '') + assert self.owner_name(game, 'A ROM') == 'TURKEY' + assert self.owner_name(game, 'F TYS') == 'ITALY' + assert self.owner_name(game, 'A APU') == 'ITALY' + assert self.owner_name(game, 'F ION') == 'TURKEY' + + def test_6_g_6(self): + """ 6.G.6. TEST CASE, SWAPPING WITH UNINTENDED INTENT + The intent is questionable. + England: A Liverpool - Edinburgh + England: F English Channel Convoys A Liverpool - Edinburgh + Germany: A Edinburgh - Liverpool + France: F Irish Sea Hold + France: F North Sea Hold + Russia: F Norwegian Sea Convoys A Liverpool - Edinburgh + Russia: F North Atlantic Ocean Convoys A Liverpool - Edinburgh + See issue 4.A.3. + For choice a, b and c the English army in Liverpool will move by convoy and consequentially the two armies + are swapped. For choice d, the 1982/2000 rulebook (which I prefer), the convoy depends on the "intent". + England intended to convoy via the French fleets in the Irish Sea and the North Sea. However, the French + did not order the convoy. The alternative route with the Russian fleets was unintended. The English fleet + in the English Channel (with the convoy order) is not part of this alternative route with the Russian + fleets. Since England still "intent" to convoy, the move from Liverpool to Edinburgh should be via convoy + and the two armies are swapped. Although, you could argue that this is not really according to the + clarification of the 2000 rulebook. When explicit adjacent convoying is used (DPTG, choice e), then the + English army did not receive an order to move by convoy. So, it is just a head to head battle and both the + army in Edinburgh and Liverpool will not move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A LVP', 'F ENG']) + self.set_units(game, 'GERMANY', ['A EDI']) + self.set_units(game, 'FRANCE', ['F IRI', 'F NTH']) + self.set_units(game, 'RUSSIA', ['F NWG', 'F NAO']) + self.set_orders(game, 'ENGLAND', ['A LVP - EDI', 'F ENG C A LVP - EDI']) + self.set_orders(game, 'GERMANY', ['A EDI - LVP']) + self.set_orders(game, 'FRANCE', ['F IRI H', 'F NTH H']) + self.set_orders(game, 'RUSSIA', ['F NWG C A LVP - EDI', 'F NAO C A LVP - EDI']) + self.process(game) + assert self.check_results(game, 'A LVP', '') + assert self.check_results(game, 'F ENG', 'no convoy') + assert self.check_results(game, 'A EDI', '') + assert self.check_results(game, 'F IRI', '') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'F NWG', '') + assert self.check_results(game, 'F NAO', '') + assert self.owner_name(game, 'A LVP') == 'GERMANY' + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + assert self.owner_name(game, 'A EDI') == 'ENGLAND' + assert self.owner_name(game, 'F IRI') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'FRANCE' + assert self.owner_name(game, 'F NWG') == 'RUSSIA' + assert self.owner_name(game, 'F NAO') == 'RUSSIA' + + def test_6_g_7(self): + """ 6.G.7. TEST CASE, SWAPPING WITH ILLEGAL INTENT + Can the intent made clear with an impossible order? + England: F Skagerrak Convoys A Sweden - Norway + England: F Norway - Sweden + Russia: A Sweden - Norway + Russia: F Gulf of Bothnia Convoys A Sweden - Norway + See issue 4.A.3 and 4.E.1. + If for issue 4.A.3 choice a, b or c has been taken, then the army in Sweden moves by convoy and swaps + places with the fleet in Norway. + However, if for issue 4.A.3 the 1982/2000 has been chosen (choice d), then the "intent" is important. + The question is whether the fleet in the Gulf of Bothnia can express the intent. If the order for this + fleet is considered illegal (see issue 4.E.1), then this order must be ignored and there is no intent to + swap. In that case none of the units move. If explicit convoying is used (DPTG, choice e of issue 4.A.3) + then the army in Sweden will take the land route and none of the units move. + I prefer the 1982/2000 rule and that any orders that can't be valid are illegal. So, the order of the fleet + in the Gulf of Bothnia is ignored and can not show the intent. There is no convoy, so no unit will move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F SKA', 'F NWY']) + self.set_units(game, 'RUSSIA', ['A SWE', 'F BOT']) + self.set_orders(game, 'ENGLAND', ['F SKA C A SWE - NWY', 'F NWY - SWE']) + self.set_orders(game, 'RUSSIA', ['A SWE - NWY', 'F BOT C A SWE - NWY']) + self.process(game) + assert self.check_results(game, 'F SKA', 'no convoy') + assert self.check_results(game, 'F NWY', 'bounce') + assert self.check_results(game, 'A SWE', 'bounce') + assert self.check_results(game, 'F BOT', 'void') + assert self.owner_name(game, 'F SKA') == 'ENGLAND' + assert self.owner_name(game, 'F NWY') == 'ENGLAND' + assert self.owner_name(game, 'A SWE') == 'RUSSIA' + assert self.owner_name(game, 'F BOT') == 'RUSSIA' + + def test_6_g_8(self): + """ 6.G.8. TEST CASE, EXPLICIT CONVOY THAT ISN'T THERE + What to do when a unit is explicitly ordered to move via convoy and the convoy is not there? + France: A Belgium - Holland via Convoy + England: F North Sea - Helgoland Bight + England: A Holland - Kiel + The French army in Belgium intended to move convoyed with the English fleet in the North Sea. But the + English changed their plans. + See issue 4.A.3. + If choice a, b or c has been taken, then the 'via Convoy' directive has no meaning and the army in Belgium + will move to Holland. If the 1982/2000 rulebook is used (choice d, which I prefer), the "via Convoy" has + meaning, but only when there is both a land route and a convoy route. Since there is no convoy the + "via Convoy" directive should be ignored. And the move from Belgium to Holland succeeds. + If explicit adjacent convoying is used (DPTG, choice e), then the unit can only go by convoy. Since there + is no convoy, the move from Belgium to Holland fails. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['A BEL']) + self.set_units(game, 'ENGLAND', ['F NTH', 'A HOL']) + self.set_orders(game, 'FRANCE', ['A BEL - HOL VIA']) + self.set_orders(game, 'ENGLAND', ['F NTH - HEL', 'A HOL - KIE']) + self.process(game) + assert self.check_results(game, 'A BEL', '') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A HOL', '') + assert self.owner_name(game, 'A BEL') is None + assert self.owner_name(game, 'F NTH') is None + assert self.owner_name(game, 'A HOL') == 'FRANCE' + assert self.owner_name(game, 'F HEL') == 'ENGLAND' + assert self.owner_name(game, 'A KIE') == 'ENGLAND' + + def test_6_g_9(self): + """ 6.G.9. TEST CASE, SWAPPED OR DISLODGED? + The 1982 rulebook says that whether the move is over land or via convoy depends on the "intent" as shown + by the totality of the orders written by the player governing the army (see issue 4.A.3). In this test + case the English army in Norway will end in all cases in Sweden. But whether it is convoyed or not has + effect on the Russian army. In case of convoy the Russian army ends in Norway and in case of a land route + the Russian army is dislodged. + England: A Norway - Sweden + England: F Skagerrak Convoys A Norway - Sweden + England: F Finland Supports A Norway - Sweden + Russia: A Sweden - Norway + See issue 4.A.3. + For choice a, b and c the move of the army in Norway is by convoy and the armies in Norway and Sweden are + swapped. If the 1982 rulebook is used with the clarification of the 2000 rulebook (choice d, which I + prefer), the intent of the English player is to convoy, since it ordered the fleet in Skagerrak to convoy. + Therefore, the armies in Norway and Sweden are swapped. When explicit adjacent convoying is used (DTPG, + choice e), then the unit in Norway did not receive an order to move by convoy and the land route should be + considered. The Russian army in Sweden is dislodged. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A NWY', 'F SKA', 'F FIN']) + self.set_units(game, 'RUSSIA', ['A SWE']) + self.set_orders(game, 'ENGLAND', ['A NWY - SWE', 'F SKA C A NWY - SWE', 'F FIN S A NWY - SWE']) + self.set_orders(game, 'RUSSIA', ['A SWE - NWY']) + self.process(game) + assert self.check_results(game, 'A NWY', '') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'F FIN', '') + assert self.check_results(game, 'A SWE', '') + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'F SKA') == 'ENGLAND' + assert self.owner_name(game, 'F FIN') == 'ENGLAND' + assert self.owner_name(game, 'A SWE') == 'ENGLAND' + + def test_6_g_10(self): + """ 6.G.10. TEST CASE, SWAPPED OR AN HEAD TO HEAD BATTLE? + Can a dislodged unit have effect on the attackers area, when the attacker moved by convoy? + England: A Norway - Sweden via Convoy + England: F Denmark Supports A Norway - Sweden + England: F Finland Supports A Norway - Sweden + Germany: F Skagerrak Convoys A Norway - Sweden + Russia: A Sweden - Norway + Russia: F Barents Sea Supports A Sweden - Norway + France: F Norwegian Sea - Norway + France: F North Sea Supports F Norwegian Sea - Norway + Since England ordered the army in Norway to move explicitly via convoy and the army in Sweden is moving + in opposite direction, only the convoyed route should be considered regardless of the rulebook used. It + is clear that the army in Norway will dislodge the Russian army in Sweden. Since the strength of three is + in all cases the strongest force. The army in Sweden will not advance to Norway, because it can not beat + the force in the Norwegian Sea. It will be dislodged by the army from Norway. + The more interesting question is whether French fleet in the Norwegian Sea is bounced by the Russian army + from Sweden. This depends on the interpretation of issue 4.A.7. If the rulebook is taken literally + (choice a), then a dislodged unit can not bounce a unit in the area where the attacker came from. This + would mean that the move of the fleet in the Norwegian Sea succeeds However, if choice b is taken + (which I prefer), then a bounce is still possible, when there is no head to head battle. So, the fleet in + the Norwegian Sea will fail to move. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A NWY', 'F DEN', 'F FIN']) + self.set_units(game, 'GERMANY', ['F SKA']) + self.set_units(game, 'RUSSIA', ['A SWE', 'F BAR']) + self.set_units(game, 'FRANCE', ['F NWG', 'F NTH']) + self.set_orders(game, 'ENGLAND', ['A NWY - SWE VIA', 'F DEN S A NWY - SWE', 'F FIN S A NWY - SWE']) + self.set_orders(game, 'GERMANY', ['F SKA C A NWY - SWE']) + self.set_orders(game, 'RUSSIA', ['A SWE - NWY', 'F BAR S A SWE - NWY']) + self.set_orders(game, 'FRANCE', ['F NWG - NWY', 'F NTH S F NWG - NWY']) + self.process(game) + assert self.check_results(game, 'A NWY', '') + assert self.check_results(game, 'F DEN', '') + assert self.check_results(game, 'F FIN', '') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'A SWE', 'bounce') + assert self.check_results(game, 'A SWE', 'dislodged') + assert self.check_results(game, 'F BAR', '') + assert self.check_results(game, 'F NWG', 'bounce') + assert self.check_results(game, 'F NTH', '') + assert check_dislodged(game, 'A SWE', 'A NWY') + assert self.owner_name(game, 'A NWY') is None + assert self.owner_name(game, 'F DEN') == 'ENGLAND' + assert self.owner_name(game, 'F FIN') == 'ENGLAND' + assert self.owner_name(game, 'F SKA') == 'GERMANY' + assert self.owner_name(game, 'A SWE') == 'ENGLAND' + assert self.owner_name(game, 'F BAR') == 'RUSSIA' + assert self.owner_name(game, 'F NWG') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'FRANCE' + + def test_6_g_11(self): + """ 6.G.11. TEST CASE, A CONVOY TO AN ADJACENT PLACE WITH A PARADOX + In this case the convoy route is available when the land route is chosen and the convoy route is not + available when the convoy route is chosen. + England: F Norway Supports F North Sea - Skagerrak + England: F North Sea - Skagerrak + Russia: A Sweden - Norway + Russia: F Skagerrak Convoys A Sweden - Norway + Russia: F Barents Sea Supports A Sweden - Norway + See issue 4.A.2 and 4.A.3. + If for issue 4.A.3, choice b, c or e has been taken, then the move from Sweden to Norway is not a + convoy and the English fleet in Norway is dislodged and the fleet in Skagerrak will not be dislodged. + If choice a or d (1982/2000 rule) has been taken for issue 4.A.3, then the move from Sweden to Norway + must be treated as a convoy. At that moment the situation becomes paradoxical. When the 'All Hold' rule is + used, both the army in Sweden as the fleet in the North Sea will not advance. In all other paradox rules + the English fleet in the North Sea will dislodge the Russian fleet in Skagerrak and the army in Sweden will + not advance. + I prefer the 1982 rule with the 2000 rulebook clarification concerning the convoy to adjacent places and + I prefer the Szykman rule for paradox resolving. That means that according to these preferences the fleet + in the North Sea will dislodge the Russian fleet in Skagerrak and the army in Sweden will not advance. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NWY', 'F NTH']) + self.set_units(game, 'RUSSIA', ['A SWE', 'F SKA', 'F BAR']) + self.set_orders(game, 'ENGLAND', ['F NWY S F NTH - SKA', 'F NTH - SKA']) + self.set_orders(game, 'RUSSIA', ['A SWE - NWY', 'F SKA C A SWE - NWY', 'F BAR S A SWE - NWY']) + self.process(game) + assert self.check_results(game, 'F NWY', '') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A SWE', 'no convoy') + assert self.check_results(game, 'F SKA', 'dislodged') + assert self.check_results(game, 'F BAR', 'no convoy') + assert check_dislodged(game, 'F SKA', 'F NTH') + assert self.owner_name(game, 'F NWY') == 'ENGLAND' + assert self.owner_name(game, 'F NTH') is None + assert self.owner_name(game, 'A SWE') == 'RUSSIA' + assert self.owner_name(game, 'F SKA') == 'ENGLAND' + assert self.owner_name(game, 'F BAR') == 'RUSSIA' + + def test_6_g_12(self): + """ 6.G.12. TEST CASE, SWAPPING TWO UNITS WITH TWO CONVOYS + Of course, two armies can also swap by when they are both convoyed. + England: A Liverpool - Edinburgh via Convoy + England: F North Atlantic Ocean Convoys A Liverpool - Edinburgh + England: F Norwegian Sea Convoys A Liverpool - Edinburgh + Germany: A Edinburgh - Liverpool via Convoy + Germany: F North Sea Convoys A Edinburgh - Liverpool + Germany: F English Channel Convoys A Edinburgh - Liverpool + Germany: F Irish Sea Convoys A Edinburgh - Liverpool + The armies in Liverpool and Edinburgh are swapped. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A LVP', 'F NAO', 'F NWG']) + self.set_units(game, 'GERMANY', ['A EDI', 'F NTH', 'F ENG', 'F IRI']) + self.set_orders(game, 'ENGLAND', ['A LVP - EDI VIA', 'F NAO C A LVP - EDI', 'F NWG C A LVP - EDI']) + self.set_orders(game, 'GERMANY', ['A EDI - LVP VIA', 'F NTH C A EDI - LVP', 'F ENG C A EDI - LVP', + 'F IRI C A EDI - LVP']) + self.process(game) + assert self.check_results(game, 'A LVP', '') + assert self.check_results(game, 'F NAO', '') + assert self.check_results(game, 'F NWG', '') + assert self.check_results(game, 'A EDI', '') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'F IRI', '') + assert self.owner_name(game, 'A LVP') == 'GERMANY' + assert self.owner_name(game, 'F NAO') == 'ENGLAND' + assert self.owner_name(game, 'F NWG') == 'ENGLAND' + assert self.owner_name(game, 'A EDI') == 'ENGLAND' + assert self.owner_name(game, 'F NTH') == 'GERMANY' + assert self.owner_name(game, 'F ENG') == 'GERMANY' + assert self.owner_name(game, 'F IRI') == 'GERMANY' + + def test_6_g_13(self): + """ 6.G.13. TEST CASE, SUPPORT CUT ON ATTACK ON ITSELF VIA CONVOY + If a unit is attacked by a supported unit, it is not possible to prevent dislodgement by trying to cut + the support. But what, if a move is attempted via a convoy? + Austria: F Adriatic Sea Convoys A Trieste - Venice + Austria: A Trieste - Venice via Convoy + Italy: A Venice Supports F Albania - Trieste + Italy: F Albania - Trieste + First it should be mentioned that if for issue 4.A.3 choice b or c is taken, then the move from Trieste + to Venice is just a move over land, because the army in Venice is not moving in opposite direction. In that + case, the support of Venice will not be cut as normal. + In any other choice for issue 4.A.3, it should be decided whether the Austrian attack is considered to be + coming from Trieste or from the Adriatic Sea. If it comes from Trieste, the support in Venice is not cut + and the army in Trieste is dislodged by the fleet in Albania. If the Austrian attack is considered to be + coming from the Adriatic Sea, then the support is cut and the army in Trieste will not be dislodged. See + also issue 4.A.4. First of all, I prefer the 1982/2000 rules for adjacent convoying. This means that I + prefer the move from Trieste uses the convoy. Furthermore, I think that the two Italian units are still + stronger than the army in Trieste. Therefore, I prefer that the support in Venice is not cut and that the + army in Trieste is dislodged by the fleet in Albania. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['F ADR', 'A TRI']) + self.set_units(game, 'ITALY', ['A VEN', 'F ALB']) + self.set_orders(game, 'AUSTRIA', ['F ADR C A TRI - VEN', 'A TRI - VEN VIA']) + self.set_orders(game, 'ITALY', ['A VEN S F ALB - TRI', 'F ALB - TRI']) + self.process(game) + assert self.check_results(game, 'F ADR', '') + assert self.check_results(game, 'A TRI', 'dislodged') + assert self.check_results(game, 'A TRI', 'bounce') + assert self.check_results(game, 'A VEN', '') + assert self.check_results(game, 'F ALB', '') + assert check_dislodged(game, 'A TRI', 'F ALB') + assert self.owner_name(game, 'F ADR') == 'AUSTRIA' + assert self.owner_name(game, 'F TRI') == 'ITALY' + assert self.owner_name(game, 'A VEN') == 'ITALY' + assert self.owner_name(game, 'F ALB') is None + + def test_6_g_14(self): + """ 6.G.14. TEST CASE, BOUNCE BY CONVOY TO ADJACENT PLACE + Similar to test case 6.G.10, but now the other unit is taking the convoy. + England: A Norway - Sweden + England: F Denmark Supports A Norway - Sweden + England: F Finland Supports A Norway - Sweden + France: F Norwegian Sea - Norway + France: F North Sea Supports F Norwegian Sea - Norway + Germany: F Skagerrak Convoys A Sweden - Norway + Russia: A Sweden - Norway via Convoy + Russia: F Barents Sea Supports A Sweden - Norway + Again the army in Sweden is bounced by the fleet in the Norwegian Sea. The army in Norway will move to + Sweden and dislodge the Russian army. + The final destination of the fleet in the Norwegian Sea depends on how issue 4.A.7 is resolved. If + choice a is taken, then the fleet advances to Norway, but if choice b is taken (which I prefer) the fleet + bounces and stays in the Norwegian Sea. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A NWY', 'F DEN', 'F FIN']) + self.set_units(game, 'FRANCE', ['F NWG', 'F NTH']) + self.set_units(game, 'GERMANY', ['F SKA']) + self.set_units(game, 'RUSSIA', ['A SWE', 'F BAR']) + self.set_orders(game, 'ENGLAND', ['A NWY - SWE', 'F DEN S A NWY - SWE', 'F FIN S A NWY - SWE']) + self.set_orders(game, 'FRANCE', ['F NWG - NWY', 'F NTH S F NWG - NWY']) + self.set_orders(game, 'GERMANY', ['F SKA C A SWE - NWY']) + self.set_orders(game, 'RUSSIA', ['A SWE - NWY VIA', 'F BAR S A SWE - NWY']) + self.process(game) + assert self.check_results(game, 'A NWY', '') + assert self.check_results(game, 'F DEN', '') + assert self.check_results(game, 'F FIN', '') + assert self.check_results(game, 'F NWG', 'bounce') + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'A SWE', 'dislodged') + assert self.check_results(game, 'A SWE', 'bounce') + assert self.check_results(game, 'F BAR', '') + assert check_dislodged(game, 'A SWE', 'A NWY') + assert self.owner_name(game, 'A NWY') is None + assert self.owner_name(game, 'F DEN') == 'ENGLAND' + assert self.owner_name(game, 'F FIN') == 'ENGLAND' + assert self.owner_name(game, 'F NWG') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'FRANCE' + assert self.owner_name(game, 'F SKA') == 'GERMANY' + assert self.owner_name(game, 'A SWE') == 'ENGLAND' + assert self.owner_name(game, 'F BAR') == 'RUSSIA' + + def test_6_g_15(self): + """ 6.G.15. TEST CASE, BOUNCE AND DISLODGE WITH DOUBLE CONVOY + Similar to test case 6.G.10, but now both units use a convoy and without some support. + England: F North Sea Convoys A London - Belgium + England: A Holland Supports A London - Belgium + England: A Yorkshire - London + England: A London - Belgium via Convoy + France: F English Channel Convoys A Belgium - London + France: A Belgium - London via Convoy + The French army in Belgium is bounced by the army from Yorkshire. The army in London move to Belgium, + dislodging the unit there. + The final destination of the army in the Yorkshire depends on how issue 4.A.7 is resolved. If choice a is + taken, then the army advances to London, but if choice b is taken (which I prefer) the army bounces and + stays in Yorkshire. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A HOL', 'A YOR', 'A LON']) + self.set_units(game, 'FRANCE', ['F ENG', 'A BEL']) + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - BEL', + 'A HOL S A LON - BEL', + 'A YOR - LON', + 'A LON - BEL VIA']) + self.set_orders(game, 'FRANCE', ['F ENG C A BEL - LON', 'A BEL - LON VIA']) + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A HOL', '') + assert self.check_results(game, 'A YOR', 'bounce') + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'A BEL', 'bounce') + assert self.check_results(game, 'A BEL', 'dislodged') + assert check_dislodged(game, 'A BEL', 'A LON') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A HOL') == 'ENGLAND' + assert self.owner_name(game, 'A YOR') == 'ENGLAND' + assert self.owner_name(game, 'A LON') is None + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'A BEL') == 'ENGLAND' + + def test_6_g_16(self): + """ 6.G.16. TEST CASE, THE TWO UNIT IN ONE AREA BUG, MOVING BY CONVOY + If the adjudicator is not correctly implemented, this may lead to a resolution where two units end up in + the same area. + England: A Norway - Sweden + England: A Denmark Supports A Norway - Sweden + England: F Baltic Sea Supports A Norway - Sweden + England: F North Sea - Norway + Russia: A Sweden - Norway via Convoy + Russia: F Skagerrak Convoys A Sweden - Norway + Russia: F Norwegian Sea Supports A Sweden - Norway + See decision details 5.B.6. If the 'PREVENT STRENGTH' is incorrectly implemented, due to the fact that it + does not take into account that the 'PREVENT STRENGTH' is only zero when the unit is engaged in a head to + head battle, then this goes wrong in this test case. The 'PREVENT STRENGTH' of Sweden would be zero, + because the opposing unit in Norway successfully moves. Since, this strength would be zero, the fleet in + the North Sea would move to Norway. However, although the 'PREVENT STRENGTH' is zero, the army in Sweden + would also move to Norway. So, the final result would contain two units that successfully moved to Norway. + Of course, this is incorrect. Norway will indeed successfully move to Sweden while the army in Sweden ends + in Norway, because it is stronger then the fleet in the North Sea. This fleet will stay in the North Sea. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A NWY', 'A DEN', 'F BAL', 'F NTH']) + self.set_units(game, 'RUSSIA', ['A SWE', 'F SKA', 'F NWG']) + self.set_orders(game, 'ENGLAND', ['A NWY - SWE', 'A DEN S A NWY - SWE', 'F BAL S A NWY - SWE', 'F NTH - NWY']) + self.set_orders(game, 'RUSSIA', ['A SWE - NWY VIA', 'F SKA C A SWE - NWY', 'F NWG S A SWE - NWY']) + self.process(game) + assert self.check_results(game, 'A NWY', '') + assert self.check_results(game, 'A DEN', '') + assert self.check_results(game, 'F BAL', '') + assert self.check_results(game, 'F NTH', 'bounce') + assert self.check_results(game, 'A SWE', '') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'F NWG', '') + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'A DEN') == 'ENGLAND' + assert self.owner_name(game, 'F BAL') == 'ENGLAND' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A SWE') == 'ENGLAND' + assert self.owner_name(game, 'F SKA') == 'RUSSIA' + assert self.owner_name(game, 'F NWG') == 'RUSSIA' + + def test_6_g_17(self): + """ 6.G.17. TEST CASE, THE TWO UNIT IN ONE AREA BUG, MOVING OVER LAND + Similar to the previous test case, but now the other unit moves by convoy. + England: A Norway - Sweden via Convoy + England: A Denmark Supports A Norway - Sweden + England: F Baltic Sea Supports A Norway - Sweden + England: F Skagerrak Convoys A Norway - Sweden + England: F North Sea - Norway + Russia: A Sweden - Norway + Russia: F Norwegian Sea Supports A Sweden - Norway + Sweden and Norway are swapped, while the fleet in the North Sea will bounce. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A NWY', 'A DEN', 'F BAL', 'F SKA', 'F NTH']) + self.set_units(game, 'RUSSIA', ['A SWE', 'F NWG']) + self.set_orders(game, 'ENGLAND', ['A NWY - SWE VIA', + 'A DEN S A NWY - SWE', + 'F BAL S A NWY - SWE', + 'F SKA C A NWY - SWE', + 'F NTH - NWY']) + self.set_orders(game, 'RUSSIA', ['A SWE - NWY', 'F NWG S A SWE - NWY']) + self.process(game) + assert self.check_results(game, 'A NWY', '') + assert self.check_results(game, 'A DEN', '') + assert self.check_results(game, 'F BAL', '') + assert self.check_results(game, 'F SKA', '') + assert self.check_results(game, 'F NTH', 'bounce') + assert self.check_results(game, 'A SWE', '') + assert self.check_results(game, 'F NWG', '') + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'A DEN') == 'ENGLAND' + assert self.owner_name(game, 'F BAL') == 'ENGLAND' + assert self.owner_name(game, 'F SKA') == 'ENGLAND' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A SWE') == 'ENGLAND' + assert self.owner_name(game, 'F NWG') == 'RUSSIA' + + def test_6_g_18(self): + """ 6.G.18. TEST CASE, THE TWO UNIT IN ONE AREA BUG, WITH DOUBLE CONVOY + Similar to the previous test case, but now both units move by convoy. + England: F North Sea Convoys A London - Belgium + England: A Holland Supports A London - Belgium + England: A Yorkshire - London + England: A London - Belgium + England: A Ruhr Supports A London - Belgium + France: F English Channel Convoys A Belgium - London + France: A Belgium - London + France: A Wales Supports A Belgium - London + Belgium and London are swapped, while the army in Yorkshire fails to move to London. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A HOL', 'A YOR', 'A LON', 'A RUH']) + self.set_units(game, 'FRANCE', ['F ENG', 'A BEL', 'A WAL']) + self.set_orders(game, 'ENGLAND', ['F NTH C A LON - BEL', + 'A HOL S A LON - BEL', + 'A YOR - LON', + 'A LON - BEL', + 'A RUH S A LON - BEL']) + self.set_orders(game, 'FRANCE', ['F ENG C A BEL - LON', 'A BEL - LON', 'A WAL S A BEL - LON']) + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A HOL', '') + assert self.check_results(game, 'A YOR', 'bounce') + assert self.check_results(game, 'A LON', '') + assert self.check_results(game, 'A RUH', '') + assert self.check_results(game, 'F ENG', '') + assert self.check_results(game, 'A BEL', '') + assert self.check_results(game, 'A WAL', '') + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A HOL') == 'ENGLAND' + assert self.owner_name(game, 'A YOR') == 'ENGLAND' + assert self.owner_name(game, 'A LON') == 'FRANCE' + assert self.owner_name(game, 'A RUH') == 'ENGLAND' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'A BEL') == 'ENGLAND' + assert self.owner_name(game, 'A WAL') == 'FRANCE' + + # 6.H. TEST CASES, RETREATING + def test_6_h_1(self): + """ 6.H.1. TEST CASE, NO SUPPORTS DURING RETREAT + Supports are not allowed in the retreat phase. + Austria: F Trieste Hold + Austria: A Serbia Hold + Turkey: F Greece Hold + Italy: A Venice Supports A Tyrolia - Trieste + Italy: A Tyrolia - Trieste + Italy: F Ionian Sea - Greece + Italy: F Aegean Sea Supports F Ionian Sea - Greece + The fleet in Trieste and the fleet in Greece are dislodged. If the retreat orders are as follows: + Austria: F Trieste - Albania + Austria: A Serbia Supports F Trieste - Albania + Turkey: F Greece - Albania + The Austrian support order is illegal. Both dislodged fleets are disbanded. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['F TRI', 'A SER']) + self.set_units(game, 'TURKEY', ['F GRE']) + self.set_units(game, 'ITALY', ['A VEN', 'A TYR', 'F ION', 'F AEG']) + + # Movement phase + self.set_orders(game, 'AUSTRIA', ['F TRI H', 'A SER H']) + self.set_orders(game, 'TURKEY', ['F GRE H']) + self.set_orders(game, 'ITALY', ['A VEN S A TYR - TRI', 'A TYR - TRI', 'F ION - GRE', 'F AEG S F ION - GRE']) + self.process(game) + assert self.check_results(game, 'F TRI', 'dislodged') + assert self.check_results(game, 'A SER', '') + assert self.check_results(game, 'F GRE', 'dislodged') + assert self.check_results(game, 'A VEN', '') + assert self.check_results(game, 'A TYR', '') + assert self.check_results(game, 'F ION', '') + assert self.check_results(game, 'F AEG', '') + assert check_dislodged(game, 'F TRI', 'A TYR') # AUSTRIA + assert check_dislodged(game, 'F GRE', 'F ION') # TURKEY + assert self.owner_name(game, 'A TRI') == 'ITALY' + assert self.owner_name(game, 'A SER') == 'AUSTRIA' + assert self.owner_name(game, 'F GRE') == 'ITALY' + assert self.owner_name(game, 'A VEN') == 'ITALY' + assert self.owner_name(game, 'A TYR') is None + assert self.owner_name(game, 'F ION') is None + assert self.owner_name(game, 'F AEG') == 'ITALY' + + # Retreats Phase + if game.phase_type == 'R': + self.set_orders(game, 'AUSTRIA', ['F TRI R ALB', 'A SER S F TRI - ALB']) + self.set_orders(game, 'TURKEY', ['F GRE R ALB']) + self.process(game) + assert self.check_results(game, 'F TRI', 'bounce', phase='R') + assert self.check_results(game, 'F TRI', 'disband', phase='R') + assert self.check_results(game, 'A SER', 'void', phase='R') + assert self.check_results(game, 'F GRE', 'bounce', phase='R') + assert self.check_results(game, 'F GRE', 'disband', phase='R') + assert not check_dislodged(game, 'F TRI', '') # AUSTRIA + assert not check_dislodged(game, 'F GRE', '') # TURKEY + assert self.owner_name(game, 'A TRI') == 'ITALY' + assert self.owner_name(game, 'A SER') == 'AUSTRIA' + assert self.owner_name(game, 'F GRE') == 'ITALY' + assert self.owner_name(game, 'A VEN') == 'ITALY' + assert self.owner_name(game, 'A TYR') is None + assert self.owner_name(game, 'F ION') is None + assert self.owner_name(game, 'F AEG') == 'ITALY' + assert self.owner_name(game, 'F ALB') is None + + def test_6_h_2(self): + """ 6.H.2. TEST CASE, NO SUPPORTS FROM RETREATING UNIT + Even a retreating unit can not give support. + England: A Liverpool - Edinburgh + England: F Yorkshire Supports A Liverpool - Edinburgh + England: F Norway Hold + Germany: A Kiel Supports A Ruhr - Holland + Germany: A Ruhr - Holland + Russia: F Edinburgh Hold + Russia: A Sweden Supports A Finland - Norway + Russia: A Finland - Norway + Russia: F Holland Hold + The English fleet in Norway and the Russian fleets in Edinburgh and Holland are dislodged. If the + following retreat orders are given: + England: F Norway - North Sea + Russia: F Edinburgh - North Sea + Russia: F Holland Supports F Edinburgh - North Sea + Although the fleet in Holland may receive an order, it may not support (it is disbanded). + The English fleet in Norway and the Russian fleet in Edinburgh bounce and are disbanded. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A LVP', 'F YOR', 'F NWY']) + self.set_units(game, 'GERMANY', ['A KIE', 'A RUH']) + self.set_units(game, 'RUSSIA', ['F EDI', 'A SWE', 'A FIN', 'F HOL']) + + # Movements Phase + self.set_orders(game, 'ENGLAND', ['A LVP - EDI', 'F YOR S A LVP - EDI', 'F NWY H']) + self.set_orders(game, 'GERMANY', ['A KIE S A RUH - HOL', 'A RUH - HOL']) + self.set_orders(game, 'RUSSIA', ['F EDI H', 'A SWE S A FIN - NWY', 'A FIN - NWY', 'F HOL H']) + self.process(game) + assert self.check_results(game, 'A LVP', '') + assert self.check_results(game, 'F YOR', '') + assert self.check_results(game, 'F NWY', 'dislodged') + assert self.check_results(game, 'A KIE', '') + assert self.check_results(game, 'A RUH', '') + assert self.check_results(game, 'F EDI', 'dislodged') + assert self.check_results(game, 'A SWE', '') + assert self.check_results(game, 'A FIN', '') + assert self.check_results(game, 'F HOL', 'dislodged') + assert check_dislodged(game, 'F NWY', 'A FIN') # ENGLAND + assert check_dislodged(game, 'F EDI', 'A LVP') # RUSSIA + assert check_dislodged(game, 'F HOL', 'A RUH') # RUSSIA + assert self.owner_name(game, 'A LVP') is None + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'A KIE') == 'GERMANY' + assert self.owner_name(game, 'A RUH') is None + assert self.owner_name(game, 'A EDI') == 'ENGLAND' + assert self.owner_name(game, 'A SWE') == 'RUSSIA' + assert self.owner_name(game, 'A FIN') is None + assert self.owner_name(game, 'A HOL') == 'GERMANY' + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ENGLAND', ['F NWY R NTH']) + self.set_orders(game, 'RUSSIA', ['F EDI R NTH', 'F HOL S F EDI - NTH']) + self.process(game) + assert self.check_results(game, 'F NWY', 'bounce', phase='R') + assert self.check_results(game, 'F NWY', 'disband', phase='R') + assert self.check_results(game, 'F EDI', 'bounce', phase='R') + assert self.check_results(game, 'F EDI', 'disband', phase='R') + assert self.check_results(game, 'F HOL', 'void', phase='R') + assert self.check_results(game, 'F EDI', 'disband', phase='R') + assert not check_dislodged(game, 'F NWY', '') # ENGLAND + assert not check_dislodged(game, 'F EDI', '') # RUSSIA + assert not check_dislodged(game, 'F HOL', '') # RUSSIA + assert self.owner_name(game, 'A LVP') is None + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'A KIE') == 'GERMANY' + assert self.owner_name(game, 'A RUH') is None + assert self.owner_name(game, 'A EDI') == 'ENGLAND' + assert self.owner_name(game, 'A SWE') == 'RUSSIA' + assert self.owner_name(game, 'A FIN') is None + assert self.owner_name(game, 'A HOL') == 'GERMANY' + assert self.owner_name(game, 'F NTH') is None + + def test_6_h_3(self): + """ 6.H.3. TEST CASE, NO CONVOY DURING RETREAT + Convoys during retreat are not allowed. + England: F North Sea Hold + England: A Holland Hold + Germany: F Kiel Supports A Ruhr - Holland + Germany: A Ruhr - Holland + The English army in Holland is dislodged. If England orders the following in retreat: + England: A Holland - Yorkshire + England: F North Sea Convoys A Holland - Yorkshire + The convoy order is illegal. The army in Holland is disbanded. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A HOL']) + self.set_units(game, 'GERMANY', ['F KIE', 'A RUH']) + + # Movements phase + self.set_orders(game, 'ENGLAND', ['F NTH H', 'A HOL H']) + self.set_orders(game, 'GERMANY', ['F KIE S A RUH - HOL', 'A RUH - HOL']) + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A HOL', 'dislodged') + assert self.check_results(game, 'F KIE', '') + assert self.check_results(game, 'A RUH', '') + assert check_dislodged(game, 'A HOL', 'A RUH') # ENGLAND + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A HOL') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'A RUH') is None + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ENGLAND', ['A HOL R YOR', 'F NTH C A HOL - YOR']) + self.process(game) + assert self.check_results(game, 'F NTH', 'void', phase='R') + assert self.check_results(game, 'A HOL', 'void', phase='R') + assert self.check_results(game, 'A HOL', 'disband', phase='R') + assert not check_dislodged(game, 'A HOL', '') # ENGLAND + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A HOL') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'A RUH') is None + assert self.owner_name(game, 'A YOR') is None + + def test_6_h_4(self): + """ 6.H.4. TEST CASE, NO OTHER MOVES DURING RETREAT + Of course you may not do any other move during a retreat. But look if the adjudicator checks for it. + England: F North Sea Hold + England: A Holland Hold + Germany: F Kiel Supports A Ruhr - Holland + Germany: A Ruhr - Holland + The English army in Holland is dislodged. If England orders the following in retreat: + England: A Holland - Belgium + England: F North Sea - Norwegian Sea + The fleet in the North Sea is not dislodge, so the move is illegal. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F NTH', 'A HOL']) + self.set_units(game, 'GERMANY', ['F KIE', 'A RUH']) + + # Movements phase + self.set_orders(game, 'ENGLAND', ['F NTH H', 'A HOL H']) + self.set_orders(game, 'GERMANY', ['F KIE S A RUH - HOL', 'A RUH - HOL']) + self.process(game) + assert self.check_results(game, 'F NTH', '') + assert self.check_results(game, 'A HOL', 'dislodged') + assert self.check_results(game, 'F KIE', '') + assert self.check_results(game, 'A RUH', '') + assert check_dislodged(game, 'A HOL', 'A RUH') # ENGLAND + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A HOL') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'A RUH') is None + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ENGLAND', ['A HOL R BEL', 'F NTH R NWG']) + self.process(game) + assert self.check_results(game, 'F NTH', 'void', phase='R') + assert self.check_results(game, 'A HOL', '', phase='R') + assert not check_dislodged(game, 'A HOL', '') # ENGLAND + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'A HOL') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'GERMANY' + assert self.owner_name(game, 'A RUH') is None + assert self.owner_name(game, 'A BEL') == 'ENGLAND' + + def test_6_h_5(self): + """ 6.H.5. TEST CASE, A UNIT MAY NOT RETREAT TO THE AREA FROM WHICH IT IS ATTACKED + Well, that would be of course stupid. Still, the adjudicator must be tested on this. + Russia: F Constantinople Supports F Black Sea - Ankara + Russia: F Black Sea - Ankara + Turkey: F Ankara Hold + Fleet in Ankara is dislodged and may not retreat to Black Sea. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'RUSSIA', ['F CON', 'F BLA']) + self.set_units(game, 'TURKEY', 'F ANK') + + # Movements phase + self.set_orders(game, 'RUSSIA', ['F CON S F BLA - ANK', 'F BLA - ANK']) + self.set_orders(game, 'TURKEY', 'F ANK H') + self.process(game) + assert self.check_results(game, 'F CON', '') + assert self.check_results(game, 'F BLA', '') + assert self.check_results(game, 'F ANK', 'dislodged') + assert check_dislodged(game, 'F ANK', 'F BLA') # TURKEY + assert self.owner_name(game, 'F CON') == 'RUSSIA' + assert self.owner_name(game, 'F BLA') is None + assert self.owner_name(game, 'F ANK') == 'RUSSIA' + + # Retreats Phase + if game.phase_type == 'R': + self.set_orders(game, 'TURKEY', ['F ANK R BLA']) + self.process(game) + assert self.check_results(game, 'F ANK', 'void', phase='R') + assert self.check_results(game, 'F ANK', 'disband', phase='R') + assert not check_dislodged(game, 'F ANK', 'F BLA') # TURKEY + assert self.owner_name(game, 'F CON') == 'RUSSIA' + assert self.owner_name(game, 'F BLA') is None + assert self.owner_name(game, 'F ANK') == 'RUSSIA' + + def test_6_h_6(self): + """ 6.H.6. TEST CASE, UNIT MAY NOT RETREAT TO A CONTESTED AREA + Stand off prevents retreat to the area. + Austria: A Budapest Supports A Trieste - Vienna + Austria: A Trieste - Vienna + Germany: A Munich - Bohemia + Germany: A Silesia - Bohemia + Italy: A Vienna Hold + The Italian army in Vienna is dislodged. It may not retreat to Bohemia. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['A BUD', 'A TRI']) + self.set_units(game, 'GERMANY', ['A MUN', 'A SIL']) + self.set_units(game, 'ITALY', ['A VIE']) + + # Movements phase + self.set_orders(game, 'AUSTRIA', ['A BUD S A TRI - VIE', 'A TRI - VIE']) + self.set_orders(game, 'GERMANY', ['A MUN - BOH', 'A SIL - BOH']) + self.set_orders(game, 'ITALY', ['A VIE H']) + self.process(game) + assert self.check_results(game, 'A BUD', '') + assert self.check_results(game, 'A TRI', '') + assert self.check_results(game, 'A MUN', 'bounce') + assert self.check_results(game, 'A SIL', 'bounce') + assert self.check_results(game, 'A VIE', 'dislodged') + assert check_dislodged(game, 'A VIE', 'A TRI') # ITALY + assert self.owner_name(game, 'A BUD') == 'AUSTRIA' + assert self.owner_name(game, 'A TRI') is None + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'A SIL') == 'GERMANY' + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A BOH') is None + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ITALY', ['A VIE R BOH']) + self.process(game) + assert self.check_results(game, 'A VIE', 'void', phase='R') + assert self.check_results(game, 'A VIE', 'disband', phase='R') + assert not check_dislodged(game, 'A VIE', '') # ITALY + assert self.owner_name(game, 'A BUD') == 'AUSTRIA' + assert self.owner_name(game, 'A TRI') is None + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'A SIL') == 'GERMANY' + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A BOH') is None + + def test_6_h_7(self): + """ 6.H.7. TEST CASE, MULTIPLE RETREAT TO SAME AREA WILL DISBAND UNITS + There can only be one unit in an area. + Austria: A Budapest Supports A Trieste - Vienna + Austria: A Trieste - Vienna + Germany: A Munich Supports A Silesia - Bohemia + Germany: A Silesia - Bohemia + Italy: A Vienna Hold + Italy: A Bohemia Hold + If Italy orders the following for retreat: + Italy: A Bohemia - Tyrolia + Italy: A Vienna - Tyrolia + Both armies will be disbanded. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'AUSTRIA', ['A BUD', 'A TRI']) + self.set_units(game, 'GERMANY', ['A MUN', 'A SIL']) + self.set_units(game, 'ITALY', ['A VIE', 'A BOH']) + + # Movements phase + self.set_orders(game, 'AUSTRIA', ['A BUD S A TRI - VIE', 'A TRI - VIE']) + self.set_orders(game, 'GERMANY', ['A MUN S A SIL - BOH', 'A SIL - BOH']) + self.set_orders(game, 'ITALY', ['A VIE H', 'A BOH H']) + self.process(game) + assert self.check_results(game, 'A BUD', '') + assert self.check_results(game, 'A TRI', '') + assert self.check_results(game, 'A MUN', '') + assert self.check_results(game, 'A SIL', '') + assert self.check_results(game, 'A VIE', 'dislodged') + assert self.check_results(game, 'A BOH', 'dislodged') + assert check_dislodged(game, 'A VIE', 'A TRI') # ITALY + assert check_dislodged(game, 'A BOH', 'A SIL') # ITALY + assert self.owner_name(game, 'A BUD') == 'AUSTRIA' + assert self.owner_name(game, 'A TRI') is None + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'A SIL') is None + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A BOH') == 'GERMANY' + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ITALY', ['A VIE R TYR', 'A BOH R TYR']) + self.process(game) + assert self.check_results(game, 'A VIE', 'bounce', phase='R') + assert self.check_results(game, 'A VIE', 'disband', phase='R') + assert self.check_results(game, 'A BOH', 'bounce', phase='R') + assert self.check_results(game, 'A BOH', 'disband', phase='R') + assert not check_dislodged(game, 'A VIE', '') # ITALY + assert not check_dislodged(game, 'A BOH', '') # ITALY + assert self.owner_name(game, 'A BUD') == 'AUSTRIA' + assert self.owner_name(game, 'A TRI') is None + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'A SIL') is None + assert self.owner_name(game, 'A VIE') == 'AUSTRIA' + assert self.owner_name(game, 'A BOH') == 'GERMANY' + assert self.owner_name(game, 'A TYR') is None + + def test_6_h_8(self): + """ 6.H.8. TEST CASE, TRIPLE RETREAT TO SAME AREA WILL DISBAND UNITS + When three units retreat to the same area, then all three units are disbanded. + England: A Liverpool - Edinburgh + England: F Yorkshire Supports A Liverpool - Edinburgh + England: F Norway Hold + Germany: A Kiel Supports A Ruhr - Holland + Germany: A Ruhr - Holland + Russia: F Edinburgh Hold + Russia: A Sweden Supports A Finland - Norway + Russia: A Finland - Norway + Russia: F Holland Hold + The fleets in Norway, Edinburgh and Holland are dislodged. If the following retreat orders are given: + England: F Norway - North Sea + Russia: F Edinburgh - North Sea + Russia: F Holland - North Sea + All three units are disbanded. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A LVP', 'F YOR', 'F NWY']) + self.set_units(game, 'GERMANY', ['A KIE', 'A RUH']) + self.set_units(game, 'RUSSIA', ['F EDI', 'A SWE', 'A FIN', 'F HOL']) + + # Movements phase + self.set_orders(game, 'ENGLAND', ['A LVP - EDI', 'F YOR S A LVP - EDI', 'F NWY H']) + self.set_orders(game, 'GERMANY', ['A KIE S A RUH - HOL', 'A RUH - HOL']) + self.set_orders(game, 'RUSSIA', ['F EDI H', 'A SWE S A FIN - NWY', 'A FIN - NWY', 'F HOL H']) + self.process(game) + assert self.check_results(game, 'A LVP', '') + assert self.check_results(game, 'F YOR', '') + assert self.check_results(game, 'F NWY', 'dislodged') + assert self.check_results(game, 'A KIE', '') + assert self.check_results(game, 'A RUH', '') + assert self.check_results(game, 'F EDI', 'dislodged') + assert self.check_results(game, 'A SWE', '') + assert self.check_results(game, 'A FIN', '') + assert self.check_results(game, 'F HOL', 'dislodged') + assert check_dislodged(game, 'F NWY', 'A FIN') # ENGLAND + assert check_dislodged(game, 'F EDI', 'A LVP') # RUSSIA + assert check_dislodged(game, 'F HOL', 'A RUH') # RUSSIA + assert self.owner_name(game, 'A LVP') is None + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'A KIE') == 'GERMANY' + assert self.owner_name(game, 'A RUH') is None + assert self.owner_name(game, 'A EDI') == 'ENGLAND' + assert self.owner_name(game, 'A SWE') == 'RUSSIA' + assert self.owner_name(game, 'A FIN') is None + assert self.owner_name(game, 'A HOL') == 'GERMANY' + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ENGLAND', ['F NWY R NTH']) + self.set_orders(game, 'RUSSIA', ['F EDI R NTH', 'F HOL R NTH']) + self.process(game) + assert self.check_results(game, 'F NWY', 'bounce', phase='R') + assert self.check_results(game, 'F NWY', 'disband', phase='R') + assert self.check_results(game, 'F EDI', 'bounce', phase='R') + assert self.check_results(game, 'F EDI', 'disband', phase='R') + assert self.check_results(game, 'F HOL', 'bounce', phase='R') + assert self.check_results(game, 'F HOL', 'disband', phase='R') + assert not check_dislodged(game, 'F NWY', '') # ENGLAND + assert not check_dislodged(game, 'F EDI', '') # RUSSIA + assert not check_dislodged(game, 'F HOL', '') # RUSSIA + assert self.owner_name(game, 'A LVP') is None + assert self.owner_name(game, 'F YOR') == 'ENGLAND' + assert self.owner_name(game, 'A NWY') == 'RUSSIA' + assert self.owner_name(game, 'A KIE') == 'GERMANY' + assert self.owner_name(game, 'A RUH') is None + assert self.owner_name(game, 'A EDI') == 'ENGLAND' + assert self.owner_name(game, 'A SWE') == 'RUSSIA' + assert self.owner_name(game, 'A FIN') is None + assert self.owner_name(game, 'A HOL') == 'GERMANY' + assert self.owner_name(game, 'F NTH') is None + + def test_6_h_9(self): + """ 6.H.9. TEST CASE, DISLODGED UNIT WILL NOT MAKE ATTACKERS AREA CONTESTED + An army can follow. + England: F Helgoland Bight - Kiel + England: F Denmark Supports F Helgoland Bight - Kiel + Germany: A Berlin - Prussia + Germany: F Kiel Hold + Germany: A Silesia Supports A Berlin - Prussia + Russia: A Prussia - Berlin + The fleet in Kiel can retreat to Berlin. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F HEL', 'F DEN']) + self.set_units(game, 'GERMANY', ['A BER', 'F KIE', 'A SIL']) + self.set_units(game, 'RUSSIA', ['A PRU']) + + # Movements phase + self.set_orders(game, 'ENGLAND', ['F HEL - KIE', 'F DEN S F HEL - KIE']) + self.set_orders(game, 'GERMANY', ['A BER - PRU', 'F KIE H', 'A SIL S A BER - PRU']) + self.set_orders(game, 'RUSSIA', ['A PRU - BER']) + self.process(game) + assert self.check_results(game, 'F HEL', '') + assert self.check_results(game, 'F DEN', '') + assert self.check_results(game, 'A BER', '') + assert self.check_results(game, 'F KIE', 'dislodged') + assert self.check_results(game, 'A SIL', '') + assert self.check_results(game, 'A PRU', 'dislodged') + assert check_dislodged(game, 'F KIE', 'F HEL') # GERMANY + assert check_dislodged(game, 'A PRU', 'A BER') # RUSSIA + assert self.owner_name(game, 'F HEL') is None + assert self.owner_name(game, 'F DEN') == 'ENGLAND' + assert self.owner_name(game, 'A BER') is None + assert self.owner_name(game, 'F KIE') == 'ENGLAND' + assert self.owner_name(game, 'A SIL') == 'GERMANY' + assert self.owner_name(game, 'A PRU') == 'GERMANY' + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'GERMANY', 'F KIE R BER') + self.process(game) + assert self.check_results(game, 'F KIE', '', phase='R') + assert self.check_results(game, 'A PRU', 'disband', phase='R') + assert not check_dislodged(game, 'F KIE', '') # GERMANY + assert not check_dislodged(game, 'A PRU', '') # RUSSIA + assert self.owner_name(game, 'F HEL') is None + assert self.owner_name(game, 'F DEN') == 'ENGLAND' + assert self.owner_name(game, 'F BER') == 'GERMANY' + assert self.owner_name(game, 'F KIE') == 'ENGLAND' + assert self.owner_name(game, 'A SIL') == 'GERMANY' + assert self.owner_name(game, 'A PRU') == 'GERMANY' + + def test_6_h_10(self): + """ 6.H.10. TEST CASE, NOT RETREATING TO ATTACKER DOES NOT MEAN CONTESTED + An army can not retreat to the place of the attacker. The easiest way to program that, is to mark that + place as "contested". However, this is not correct. Another army may retreat to that place. + England: A Kiel Hold + Germany: A Berlin - Kiel + Germany: A Munich Supports A Berlin - Kiel + Germany: A Prussia Hold + Russia: A Warsaw - Prussia + Russia: A Silesia Supports A Warsaw - Prussia + The armies in Kiel and Prussia are dislodged. The English army in Kiel can not retreat to Berlin, but + the army in Prussia can retreat to Berlin. Suppose the following retreat orders are given: + England: A Kiel - Berlin + Germany: A Prussia - Berlin + The English retreat to Berlin is illegal and fails (the unit is disbanded). The German retreat to Berlin is + successful and does not bounce on the English unit. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A KIE']) + self.set_units(game, 'GERMANY', ['A BER', 'A MUN', 'A PRU']) + self.set_units(game, 'RUSSIA', ['A WAR', 'A SIL']) + + # Movements phase + self.set_orders(game, 'ENGLAND', ['A KIE H']) + self.set_orders(game, 'GERMANY', ['A BER - KIE', 'A MUN S A BER - KIE', 'A PRU H']) + self.set_orders(game, 'RUSSIA', ['A WAR - PRU', 'A SIL S A WAR - PRU']) + self.process(game) + assert self.check_results(game, 'A KIE', 'dislodged') + assert self.check_results(game, 'A BER', '') + assert self.check_results(game, 'A MUN', '') + assert self.check_results(game, 'A PRU', 'dislodged') + assert self.check_results(game, 'A WAR', '') + assert self.check_results(game, 'A SIL', '') + assert check_dislodged(game, 'A KIE', 'A BER') # ENGLAND + assert check_dislodged(game, 'A PRU', 'A WAR') # GERMANY + assert self.owner_name(game, 'A KIE') == 'GERMANY' + assert self.owner_name(game, 'A BER') is None + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'A PRU') == 'RUSSIA' + assert self.owner_name(game, 'A WAR') is None + assert self.owner_name(game, 'A SIL') == 'RUSSIA' + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ENGLAND', ['A KIE R BER']) + self.set_orders(game, 'GERMANY', ['A PRU R BER']) + self.process(game) + assert self.check_results(game, 'A KIE', 'void', phase='R') + assert self.check_results(game, 'A PRU', '', phase='R') + assert not check_dislodged(game, 'A KIE', '') # ENGLAND + assert not check_dislodged(game, 'A PRU', '') # GERMANY + assert self.owner_name(game, 'A KIE') == 'GERMANY' + assert self.owner_name(game, 'A BER') == 'GERMANY' + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'A PRU') == 'RUSSIA' + assert self.owner_name(game, 'A WAR') is None + assert self.owner_name(game, 'A SIL') == 'RUSSIA' + + def test_6_h_11(self): + """ 6.H.11. TEST CASE, RETREAT WHEN DISLODGED BY ADJACENT CONVOY + If a unit is dislodged by an army via convoy, the question arises whether the dislodged army can retreat + to the original place of the convoyed army. This is only relevant in case the convoy was to an adjacent + place. + France: A Gascony - Marseilles via Convoy + France: A Burgundy Supports A Gascony - Marseilles + France: F Mid-Atlantic Ocean Convoys A Gascony - Marseilles + France: F Western Mediterranean Convoys A Gascony - Marseilles + France: F Gulf of Lyon Convoys A Gascony - Marseilles + Italy: A Marseilles Hold + If for issue 4.A.3 choice b or c has been taken, then the army in Gascony will not move with the use of + the convoy, because the army in Marseilles does not move in opposite direction. This immediately means that + the army in Marseilles may not move to Gascony when it dislodged by the army there. + For all other choices of issue 4.A.3, the army in Gascony takes a convoy and does not pass the border of + Gascony with Marseilles (it went a complete different direction). Now, the result depends on which rule + is used for retreating (see issue 4.A.5). + I prefer the 1982/2000 rule for convoying to adjacent places. This means that the move of Gascony happened + by convoy. Furthermore, I prefer that the army in Marseilles may retreat to Gascony. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['A GAS', 'A BUR', 'F MAO', 'F WES', 'F LYO']) + self.set_units(game, 'ITALY', ['A MAR']) + + # Movements phase + self.set_orders(game, 'FRANCE', ['A GAS - MAR VIA', 'A BUR S A GAS - MAR', 'F MAO C A GAS - MAR', + 'F WES C A GAS - MAR', 'F LYO C A GAS - MAR']) + self.set_orders(game, 'ITALY', ['A MAR H']) + self.process(game) + assert self.check_results(game, 'A GAS', '') + assert self.check_results(game, 'A BUR', '') + assert self.check_results(game, 'F MAO', '') + assert self.check_results(game, 'F WES', '') + assert self.check_results(game, 'F LYO', '') + assert self.check_results(game, 'A MAR', 'dislodged') + assert check_dislodged(game, 'A MAR', 'A GAS') # ITALY + assert self.owner_name(game, 'A GAS') is None + assert self.owner_name(game, 'A BUR') == 'FRANCE' + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'F WES') == 'FRANCE' + assert self.owner_name(game, 'F LYO') == 'FRANCE' + assert self.owner_name(game, 'A MAR') == 'FRANCE' + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ITALY', ['A MAR R GAS']) + self.process(game) + assert self.check_results(game, 'A MAR', '', phase='R') + assert not check_dislodged(game, 'A MAR', '') # ITALY + assert self.owner_name(game, 'A GAS') == 'ITALY' + assert self.owner_name(game, 'A BUR') == 'FRANCE' + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'F WES') == 'FRANCE' + assert self.owner_name(game, 'F LYO') == 'FRANCE' + assert self.owner_name(game, 'A MAR') == 'FRANCE' + + def test_6_h_12(self): + """ 6.H.12. TEST CASE, RETREAT WHEN DISLODGED BY ADJACENT CONVOY WHILE TRYING TO DO THE SAME + The previous test case can be made more extra ordinary, when both armies tried to move by convoy. + England: A Liverpool - Edinburgh via Convoy + England: F Irish Sea Convoys A Liverpool - Edinburgh + England: F English Channel Convoys A Liverpool - Edinburgh + England: F North Sea Convoys A Liverpool - Edinburgh + France: F Brest - English Channel + France: F Mid-Atlantic Ocean Supports F Brest - English Channel + Russia: A Edinburgh - Liverpool via Convoy + Russia: F Norwegian Sea Convoys A Edinburgh - Liverpool + Russia: F North Atlantic Ocean Convoys A Edinburgh - Liverpool + Russia: A Clyde Supports A Edinburgh - Liverpool + If for issue 4.A.3 choice c has been taken, then the army in Liverpool will not try to move by convoy, + because the convoy is disrupted. This has as consequence that army will just advance to Edinburgh by using + the land route. For all other choices of issue 4.A.3, both the army in Liverpool as in Edinburgh will try + to move by convoy. The army in Edinburgh will succeed. The army in Liverpool will fail, because of the + disrupted convoy. It is dislodged by the army of Edinburgh. Now, the question is whether the army in + Liverpool may retreat to Edinburgh. The result depends on which rule is used for retreating (see issue + 4.A.5). I prefer the 1982/2000 rule for convoying to adjacent places. This means that the army in Liverpool + tries the disrupted convoy. Furthermore, I prefer that the army in Liverpool may retreat to Edinburgh. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A LVP', 'F IRI', 'F ENG', 'F NTH']) + self.set_units(game, 'FRANCE', ['F BRE', 'F MAO']) + self.set_units(game, 'RUSSIA', ['A EDI', 'F NWG', 'F NAO', 'A CLY']) + + # Movements phase + self.set_orders(game, 'ENGLAND', ['A LVP - EDI VIA', + 'F IRI C A LVP - EDI', + 'F ENG C A LVP - EDI', + 'F NTH C A LVP - EDI']) + self.set_orders(game, 'FRANCE', ['F BRE - ENG', 'F MAO S F BRE - ENG']) + self.set_orders(game, 'RUSSIA', ['A EDI - LVP VIA', + 'F NWG C A EDI - LVP', + 'F NAO C A EDI - LVP', + 'A CLY S A EDI - LVP']) + self.process(game) + assert self.check_results(game, 'A LVP', 'dislodged') + assert self.check_results(game, 'F IRI', 'no convoy') + assert self.check_results(game, 'F ENG', 'dislodged') + assert self.check_results(game, 'F NTH', 'no convoy') + assert self.check_results(game, 'F BRE', '') + assert self.check_results(game, 'F MAO', '') + assert self.check_results(game, 'A EDI', '') + assert self.check_results(game, 'F NWG', '') + assert self.check_results(game, 'F NAO', '') + assert self.check_results(game, 'A CLY', '') + assert check_dislodged(game, 'F ENG', 'F BRE') # ENGLAND + assert check_dislodged(game, 'A LVP', 'A EDI') # ENGLAND + assert self.owner_name(game, 'A LVP') == 'RUSSIA' + assert self.owner_name(game, 'F IRI') == 'ENGLAND' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'F BRE') is None + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'A EDI') is None + assert self.owner_name(game, 'F NWG') == 'RUSSIA' + assert self.owner_name(game, 'F NAO') == 'RUSSIA' + assert self.owner_name(game, 'A CLY') == 'RUSSIA' + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ENGLAND', ['A LVP R EDI', 'F ENG D']) + self.process(game) + assert self.check_results(game, 'A LVP', '', phase='R') + assert self.check_results(game, 'F ENG', 'disband', phase='R') + assert not check_dislodged(game, 'A LVP', '') # ENGLAND + assert not check_dislodged(game, 'F ENG', '') # ENGLAND + assert self.owner_name(game, 'A LVP') == 'RUSSIA' + assert self.owner_name(game, 'F IRI') == 'ENGLAND' + assert self.owner_name(game, 'F ENG') == 'FRANCE' + assert self.owner_name(game, 'F NTH') == 'ENGLAND' + assert self.owner_name(game, 'F BRE') is None + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'A EDI') == 'ENGLAND' + assert self.owner_name(game, 'F NWG') == 'RUSSIA' + assert self.owner_name(game, 'F NAO') == 'RUSSIA' + assert self.owner_name(game, 'A CLY') == 'RUSSIA' + + def test_6_h_13(self): + """ 6.H.13. TEST CASE, NO RETREAT WITH CONVOY IN MAIN PHASE + The places where a unit may retreat to, must be calculated during the main phase. Care should be taken + that a convoy ordered in the main phase can not be used in the retreat phase. + England: A Picardy Hold + England: F English Channel Convoys A Picardy - London + France: A Paris - Picardy + France: A Brest Supports A Paris - Picardy + The dislodged army in Picardy can not retreat to London. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A PIC', 'F ENG']) + self.set_units(game, 'FRANCE', ['A PAR', 'A BRE']) + + # Movements phase + self.set_orders(game, 'ENGLAND', ['A PIC H', 'F ENG C A PIC - LON']) + self.set_orders(game, 'FRANCE', ['A PAR - PIC', 'A BRE S A PAR - PIC']) + self.process(game) + assert self.check_results(game, 'A PIC', 'dislodged') + assert self.check_results(game, 'F ENG', 'void') + assert self.check_results(game, 'A PAR', '') + assert self.check_results(game, 'A BRE', '') + assert check_dislodged(game, 'A PIC', 'A PAR') # ENGLAND + assert self.owner_name(game, 'A PIC') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + assert self.owner_name(game, 'A PAR') is None + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'A LON') is None + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ENGLAND', 'A PIC R LON') + self.process(game) + assert self.check_results(game, 'A PIC', 'void', phase='R') + assert self.check_results(game, 'A PIC', 'disband', phase='R') + assert not check_dislodged(game, 'A PIC', '') # ENGLAND + assert self.owner_name(game, 'A PIC') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + assert self.owner_name(game, 'A PAR') is None + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'A LON') is None + + def test_6_h_14(self): + """ 6.H.14. TEST CASE, NO RETREAT WITH SUPPORT IN MAIN PHASE + Comparable to the previous test case, a support given in the main phase can not be used in the retreat + phase. + England: A Picardy Hold + England: F English Channel Supports A Picardy - Belgium + France: A Paris - Picardy + France: A Brest Supports A Paris - Picardy + France: A Burgundy Hold + Germany: A Munich Supports A Marseilles - Burgundy + Germany: A Marseilles - Burgundy + After the main phase the following retreat orders are given: + England: A Picardy - Belgium + France: A Burgundy - Belgium + Both the army in Picardy and Burgundy are disbanded. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['A PIC', 'F ENG']) + self.set_units(game, 'FRANCE', ['A PAR', 'A BRE', 'A BUR']) + self.set_units(game, 'GERMANY', ['A MUN', 'A MAR']) + + # Movements phase + self.set_orders(game, 'ENGLAND', ['A PIC H', 'F ENG S A PIC - BEL']) + self.set_orders(game, 'FRANCE', ['A PAR - PIC', 'A BRE S A PAR - PIC', 'A BUR H']) + self.set_orders(game, 'GERMANY', ['A MUN S A MAR - BUR', 'A MAR - BUR']) + self.process(game) + assert self.check_results(game, 'A PIC', 'dislodged') + assert self.check_results(game, 'F ENG', 'void') + assert self.check_results(game, 'A PAR', '') + assert self.check_results(game, 'A BRE', '') + assert self.check_results(game, 'A BUR', 'dislodged') + assert self.check_results(game, 'A MUN', '') + assert self.check_results(game, 'A MAR', '') + assert check_dislodged(game, 'A PIC', 'A PAR') # ENGLAND + assert check_dislodged(game, 'A BUR', 'A MAR') # FRANCE + assert self.owner_name(game, 'A PIC') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + assert self.owner_name(game, 'A PAR') is None + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'A BUR') == 'GERMANY' + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'A MAR') is None + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ENGLAND', ['A PIC R BEL']) + self.set_orders(game, 'FRANCE', ['A BUR R BEL']) + self.process(game) + assert self.check_results(game, 'A PIC', 'bounce', phase='R') + assert self.check_results(game, 'A PIC', 'disband', phase='R') + assert self.check_results(game, 'A BUR', 'bounce', phase='R') + assert self.check_results(game, 'A BUR', 'disband', phase='R') + assert not check_dislodged(game, 'A PIC', '') # ENGLAND + assert not check_dislodged(game, 'A BUR', '') # FRANCE + assert self.owner_name(game, 'A PIC') == 'FRANCE' + assert self.owner_name(game, 'F ENG') == 'ENGLAND' + assert self.owner_name(game, 'A PAR') is None + assert self.owner_name(game, 'A BRE') == 'FRANCE' + assert self.owner_name(game, 'A BUR') == 'GERMANY' + assert self.owner_name(game, 'A MUN') == 'GERMANY' + assert self.owner_name(game, 'A MAR') is None + + def test_6_h_15(self): + """ 6.H.15. TEST CASE, NO COASTAL CRAWL IN RETREAT + You can not go to the other coast from where the attacker came from. + England: F Portugal Hold + France: F Spain(sc) - Portugal + France: F Mid-Atlantic Ocean Supports F Spain(sc) - Portugal + The English fleet in Portugal is destroyed and can not retreat to Spain(nc). + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'ENGLAND', ['F POR']) + self.set_units(game, 'FRANCE', ['F SPA/SC', 'F MAO']) + + # Movements phase + self.set_orders(game, 'ENGLAND', ['F POR H']) + self.set_orders(game, 'FRANCE', ['F SPA/SC - POR', 'F MAO S F SPA/SC - POR']) + self.process(game) + assert self.check_results(game, 'F POR', 'dislodged') + assert self.check_results(game, 'F SPA/SC', '') + assert self.check_results(game, 'F MAO', '') + assert check_dislodged(game, 'F POR', 'F SPA/SC') # ENGLAND + assert self.owner_name(game, 'F POR') == 'FRANCE' + assert self.owner_name(game, 'F SPA') is None + assert self.owner_name(game, 'F SPA/NC') is None + assert self.owner_name(game, 'F SPA/SC') is None + assert self.owner_name(game, 'F MAO') == 'FRANCE' + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'ENGLAND', 'F POR R SPA/NC') + self.process(game) + assert self.check_results(game, 'F POR', 'void', phase='R') + assert self.check_results(game, 'F POR', 'disband', phase='R') + assert not check_dislodged(game, 'F POR', '') # ENGLAND + assert self.owner_name(game, 'F POR') == 'FRANCE' + assert self.owner_name(game, 'F SPA') is None + assert self.owner_name(game, 'F SPA/NC') is None + assert self.owner_name(game, 'F SPA/SC') is None + assert self.owner_name(game, 'F MAO') == 'FRANCE' + + def test_6_h_16(self): + """ 6.H.16. TEST CASE, CONTESTED FOR BOTH COASTS + If a coast is contested, the other is not available for retreat. + France: F Mid-Atlantic Ocean - Spain(nc) + France: F Gascony - Spain(nc) + France: F Western Mediterranean Hold + Italy: F Tunis Supports F Tyrrhenian Sea - Western Mediterranean + Italy: F Tyrrhenian Sea - Western Mediterranean + The French fleet in the Western Mediterranean can not retreat to Spain(sc). + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['F MAO', 'F GAS', 'F WES']) + self.set_units(game, 'ITALY', ['F TUN', 'F TYS']) + + # Movements phase + self.set_orders(game, 'FRANCE', ['F MAO - SPA/NC', 'F GAS - SPA/NC', 'F WES H']) + self.set_orders(game, 'ITALY', ['F TUN S F TYS - WES', 'F TYS - WES']) + self.process(game) + assert self.check_results(game, 'F MAO', 'bounce') + assert self.check_results(game, 'F GAS', 'bounce') + assert self.check_results(game, 'F WES', 'dislodged') + assert self.check_results(game, 'F TUN', '') + assert self.check_results(game, 'F TYS', '') + assert check_dislodged(game, 'F WES', 'F TYS') # FRANCE + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'F GAS') == 'FRANCE' + assert self.owner_name(game, 'F WES') == 'ITALY' + assert self.owner_name(game, 'F TUN') == 'ITALY' + assert self.owner_name(game, 'F TYS') is None + assert self.owner_name(game, 'F SPA') is None + assert self.owner_name(game, 'F SPA/NC') is None + assert self.owner_name(game, 'F SPA/SC') is None + + # Retreats phase + if game.phase_type == 'R': + self.set_orders(game, 'FRANCE', 'F WES R SPA/SC') + self.process(game) + assert self.check_results(game, 'F WES', 'void', phase='R') + assert self.check_results(game, 'F WES', 'disband', phase='R') + assert not check_dislodged(game, 'F WES', '') # FRANCE + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'F GAS') == 'FRANCE' + assert self.owner_name(game, 'F WES') == 'ITALY' + assert self.owner_name(game, 'F TUN') == 'ITALY' + assert self.owner_name(game, 'F TYS') is None + assert self.owner_name(game, 'F SPA') is None + assert self.owner_name(game, 'F SPA/NC') is None + assert self.owner_name(game, 'F SPA/SC') is None + + # 6.I. TEST CASES, BUILDING + def test_6_i_1(self): + """ 6.I.1. TEST CASE, TOO MANY BUILD ORDERS + Check how program reacts when someone orders too many builds. + Germany may build one: + Germany: Build A Warsaw + Germany: Build A Kiel + Germany: Build A Munich + Program should not build all three, but handle it in an other way. See issue 4.D.4. + I prefer that the build orders are just handled one by one until all allowed units are build. According + to this preference, the build in Warsaw fails, the build in Kiel succeeds and the build in Munich fails. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'GERMANY', ['KIE', 'MUN', 'WAR']) + self.set_units(game, 'GERMANY', ['F NAO', 'F MAO']) + self.move_to_phase(game, 'W1901A') + if game.phase_type == 'A': + self.set_orders(game, 'GERMANY', ['A WAR B', 'A KIE B', 'A MUN B']) + self.process(game) + assert self.check_results(game, 'A WAR', ['void'], phase='A') + assert self.check_results(game, 'A KIE', [''], phase='A') + assert self.check_results(game, 'A MUN', ['void'], phase='A') + assert self.owner_name(game, 'A WAR') is None + assert self.owner_name(game, 'A KIE') == 'GERMANY' + assert self.owner_name(game, 'A MUN') is None + + def test_6_i_2(self): + """ 6.I.2. TEST CASE, FLEETS CAN NOT BE BUILD IN LAND AREAS + Physical this is possible, but it is still not allowed. + Russia has one build and Moscow is empty. + Russia: Build F Moscow + See issue 4.C.4. Some game masters will change the order and build an army in Moscow. + I prefer that the build fails. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', 'MOS') + self.move_to_phase(game, 'W1901A') + if game.phase_type == 'A': + self.set_orders(game, 'RUSSIA', ['F MOS B']) + self.process(game) + assert self.check_results(game, 'F MOS', ['void'], phase='A') + assert self.owner_name(game, 'F MOS') is None + + def test_6_i_3(self): + """ 6.I.3. TEST CASE, SUPPLY CENTER MUST BE EMPTY FOR BUILDING + You can't have two units in a sector. So, you can't build when there is a unit in the supply center. + Germany may build a unit but has an army in Berlin. Germany orders the following: + Germany: Build A Berlin + Build fails. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'GERMANY', ['MUN', 'BER']) + self.set_units(game, 'GERMANY', ['A BER']) + self.move_to_phase(game, 'W1901A') + if game.phase_type == 'A': + self.set_orders(game, 'GERMANY', ['A BER B']) + self.process(game) + assert self.check_results(game, 'A BER', ['void'], phase='A') + assert self.owner_name(game, 'A BER') == 'GERMANY' + + def test_6_i_4(self): + """ 6.I.4. TEST CASE, BOTH COASTS MUST BE EMPTY FOR BUILDING + If a sector is occupied on one coast, the other coast can not be used for building. + Russia may build a unit and has a fleet in St Petersburg(sc). Russia orders the following: + Russia: Build A St Petersburg(nc) + Build fails. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', ['STP', 'MOS']) + self.set_units(game, 'RUSSIA', ['F STP/SC']) + self.move_to_phase(game, 'W1901A') + if game.phase_type == 'A': + self.set_orders(game, 'RUSSIA', ['F STP/NC B']) + self.process(game) + assert self.check_results(game, 'F STP/NC', ['void'], phase='A') + assert self.owner_name(game, 'F STP') == 'RUSSIA' + assert self.owner_name(game, 'F STP/NC') is None + assert self.owner_name(game, 'F STP/SC') == 'RUSSIA' + + def test_6_i_5(self): + """ 6.I.5. TEST CASE, BUILDING IN HOME SUPPLY CENTER THAT IS NOT OWNED + Building a unit is only allowed when supply center is a home supply center and is owned. If not owned, + build fails. + Russia captured Berlin in Fall. Left Berlin. Germany can not build in Berlin. + Germany: Build A Berlin + Build fails. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'GERMANY', 'MUN') + self.set_centers(game, 'RUSSIA', 'BER') + self.move_to_phase(game, 'W1901A') + if game.phase_type == 'A': + self.set_orders(game, 'GERMANY', ['A BER B']) + self.process(game) + assert self.check_results(game, 'A BER', ['void'], phase='A') + assert self.owner_name(game, 'A BER') is None + + def test_6_i_6(self): + """ 6.I.6. TEST CASE, BUILDING IN OWNED SUPPLY CENTER THAT IS NOT A HOME SUPPLY CENTER + Building a unit is only allowed when supply center is a home supply center and is owned. If it is not + a home supply center, the build fails. + Germany owns Warsaw, Warsaw is empty and Germany may build one unit. + Germany: + Build A Warsaw + Build fails. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'GERMANY', ['MUN', 'WAR']) + self.set_units(game, 'GERMANY', 'A MUN') + self.move_to_phase(game, 'W1901A') + if game.phase_type == 'A': + self.set_orders(game, 'GERMANY', ['A WAR B']) + self.process(game) + assert self.check_results(game, 'A WAR', ['void'], phase='A') + assert self.owner_name(game, 'A WAR') is None + + def test_6_i_7(self): + """ 6.I.7. TEST CASE, ONLY ONE BUILD IN A HOME SUPPLY CENTER + If you may build two units, you can still only build one in a supply center. + Russia owns Moscow, Moscow is empty and Russia may build two units. + Russia: Build A Moscow + Russia: Build A Moscow + The second build should fail. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', ['STP', 'MOS']) + self.move_to_phase(game, 'W1901A') + if game.phase_type == 'A': + self.set_orders(game, 'RUSSIA', ['A MOS B', 'A MOS B']) + self.process(game) + assert self.check_results(game, 'A MOS', ['void', ''], phase='A') + assert self.owner_name(game, 'A MOS') == 'RUSSIA' + + # 6.J. TEST CASES, CIVIL DISORDER AND DISBANDS + def test_6_j_1(self): + """ 6.J.1. TEST CASE, TOO MANY REMOVE ORDERS + Check how program reacts when someone orders too disbands. + France has to disband one and has an army in Paris and Picardy. + France: Remove F Gulf of Lyon + France: Remove A Picardy + France: Remove A Paris + Program should not disband both Paris and Picardy, but should handle it in a different way. See also + issue 4.D.6. I prefer that the disband orders are handled one by one. According to the preference, the + removal of the fleet in the Gulf of Lyon fails (no fleet), the removal of the army in Picardy succeeds and + the removal of the army in Paris fails (too many disbands). + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'FRANCE', 'PAR') + self.set_units(game, 'FRANCE', ['A PIC', 'A PAR']) + self.move_to_phase(game, 'W1901A') + if game.phase_type == 'A': + self.set_orders(game, 'FRANCE', ['F LYO D', 'A PIC D', 'A PAR D']) + self.process(game) + assert self.check_results(game, 'F LYO', ['void'], phase='A') + assert self.check_results(game, 'A PIC', [''], phase='A') + assert self.check_results(game, 'A PAR', ['void'], phase='A') + assert self.owner_name(game, 'F LYO') is None + assert self.owner_name(game, 'A PIC') is None + assert self.owner_name(game, 'A PAR') == 'FRANCE' + + def test_6_j_2(self): + """ 6.J.2. TEST CASE, REMOVING THE SAME UNIT TWICE + If you have to remove two units, you can always try to trick the computer by removing the same unit twice. + France has to disband two and has an army in Paris. + France: Remove A Paris + France: Remove A Paris + Program should remove army in Paris and remove another unit by using the civil disorder rules. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'FRANCE', 'PAR') + self.set_units(game, 'FRANCE', ['A PIC', 'A PAR', 'F NAO']) + self.move_to_phase(game, 'W1901A') + self.set_orders(game, 'FRANCE', ['A PAR D', 'A PAR D']) + self.process(game) + assert self.check_results(game, 'A PAR', ['void', ''], phase='A') + assert self.owner_name(game, 'A PAR') is None + assert self.owner_name(game, 'A PIC') is None or self.owner_name(game, 'F NAO') is None + + def test_6_j_3(self): + """ 6.J.3. TEST CASE, CIVIL DISORDER TWO ARMIES WITH DIFFERENT DISTANCE + When a player forgets to disband a unit, the civil disorder rules must be applied. When two armies + have different distance from the home supply centers, then the army with the greatest distance has to + be removed. + Russia has to remove one. + Russia has armies in Livonia and Sweden. + Russia does not order a disband. + The army in Sweden is removed. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', 'SWE') + self.set_units(game, 'RUSSIA', ['A LVN', 'A SWE']) + self.move_to_phase(game, 'W1901A') + self.process(game) + assert self.owner_name(game, 'A LVN') == 'RUSSIA' + assert self.owner_name(game, 'A SWE') is None + + def test_6_j_4(self): + """ 6.J.4. TEST CASE, CIVIL DISORDER TWO ARMIES WITH EQUAL DISTANCE + If two armies have equal distance from the home supply centers, then alphabetical order is used. + Russia has to remove one. + Russia has armies in Livonia and Ukraine. + Russia does not order a disband. + Both armies have distance one. The Livonia army is removed, because it appears first in alphabetical order. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', 'STP') + self.set_units(game, 'RUSSIA', ['A LIV', 'A UKR']) + self.move_to_phase(game, 'W1901A') + self.process(game) + assert self.owner_name(game, 'A LIV') is None + assert self.owner_name(game, 'A UKR') == 'RUSSIA' + + def test_6_j_5(self): + """ 6.J.5 TEST CASE, CIVIL DISORDER TWO FLEETS WITH DIFFERENT DISTANCE + If two fleets have different distance from the home supply centers, then the fleet with the greatest + distance has to be removed. Note that fleets can not go over land. + Russia has to remove one. + Russia has fleets in Skagerrak and Berlin. + Russia does not order a disband. + The distance of the fleet in Berlin is three (the fleet can not go to Warsaw), the fleet in Skaggerrak + has distance two (via Norway). So, the fleet in Berlin has to be removed. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', 'BER') + self.set_units(game, 'RUSSIA', ['F SKA', 'F BER']) + self.move_to_phase(game, 'W1901A') + self.process(game) + assert self.owner_name(game, 'F SKA') == 'RUSSIA' + assert self.owner_name(game, 'F BER') is None + + def test_6_j_6(self): + """ 6.J.6. TEST CASE, CIVIL DISORDER TWO FLEETS WITH EQUAL DISTANCE + Alphabetical order is used, when two fleets have equal distance to the home supply centers. + Russia has to remove one. + Russia has fleets in Berlin and Helgoland Bight. + Russia does not order a disband. + The distances of both fleets to one of the home supply centers is three. The fleet in the Berlin is + removed, because it appears first in alphabetical order. This also tests whether fleets can not go over + land. If they could go over land, the distance of Berlin would be two (going to Warsaw) and the fleet in + the Helgoland Bight would have incorrectly be removed. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', 'BER') + self.set_units(game, 'RUSSIA', ['F BER', 'F HEL']) + self.move_to_phase(game, 'W1901A') + self.process(game) + assert self.owner_name(game, 'F BER') is None + assert self.owner_name(game, 'F HEL') == 'RUSSIA' + + def test_6_j_7(self): + """ 6.J.7. TEST CASE, CIVIL DISORDER TWO FLEETS AND ARMY WITH EQUAL DISTANCE + In removal, the fleet has precedence over an army. In this case there are two fleets, to make the test + more complex. + Russia has to remove one. + Russia has an army in Bohemia, a fleet in Skagerrak and a fleet in the North Sea. + Russia does not order a disband. + The distances of the army and the fleets to one of the home supply centers are two. The fleets take + precedence above the army (although the army is alphabetical first). The fleet in the North Sea is + alphabetical first, compared to Skagerrak and has to be removed. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', ['STP', 'MOS']) + self.set_units(game, 'RUSSIA', ['A BOH', 'F SKA', 'F NTH']) + self.move_to_phase(game, 'W1901A') + self.process(game) + assert self.owner_name(game, 'A BOH') == 'RUSSIA' + assert self.owner_name(game, 'F SKA') == 'RUSSIA' + assert self.owner_name(game, 'F NTH') is None + + def test_6_j_8(self): + """ 6.J.8. TEST CASE, CIVIL DISORDER A FLEET WITH SHORTER DISTANCE THEN THE ARMY + If the fleet has a shorter distance than the army, the army is removed. + Russia has to remove one. + Russia has an army in Tyrolia and a fleet in the Baltic Sea. + Russia does not order a disband. + The distances of the army to Warsaw is three while the distance of the fleet is two. So, the army + is removed. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', 'STP') + self.set_units(game, 'RUSSIA', ['A TYR', 'F BAL']) + self.move_to_phase(game, 'W1901A') + self.process(game) + assert self.owner_name(game, 'A TYR') is None + assert self.owner_name(game, 'F BAL') == 'RUSSIA' + + def test_6_j_9(self): + """ 6.J.9. TEST CASE, CIVIL DISORDER MUST BE COUNTED FROM BOTH COASTS + Distance must be calculated from both coasts. + + a) + Russia has to remove one. + Russia has an army in Tyrolia and a fleet in the Baltic Sea. + Russia does not order a disband. + The distance of the fleet to St Petersburg(nc) is three but to St Petersburg(sc) is two. So, the army + in Tyrolia must be removed. + + b) + Russia has to remove one. + Russia has an army in Tyrolia and a fleet in Skagerrak. + Russia does not order a disband. + The distance of the fleet to St Petersburg(sc) is three but to St Petersburg(nc) is two. So, the army + in Tyrolia must be removed. + """ + # a) + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', 'STP') + self.set_units(game, 'RUSSIA', ['A TYR', 'F BAL']) + self.move_to_phase(game, 'W1901A') + self.process(game) + assert self.owner_name(game, 'A TYR') is None + assert self.owner_name(game, 'F BAL') == 'RUSSIA' + + # b) + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'RUSSIA', 'STP') + self.set_units(game, 'RUSSIA', ['A TYR', 'F SKA']) + self.move_to_phase(game, 'W1901A') + self.process(game) + assert self.owner_name(game, 'A TYR') is None + assert self.owner_name(game, 'F SKA') == 'RUSSIA' + + def test_6_j_10(self): + """ 6.J.10. TEST CASE, CIVIL DISORDER COUNTING CONVOYING DISTANCE + For armies the distance must be calculated by taking land areas, coastal areas as sea areas. + Italy has to remove one. + Italy has a fleet in the Ionian Sea and armies in Greece and Silesia. + Italy does not order a disband. + The distance from Greece to one of the Italian home supply center is three over land. However, using + a convoy the distance is one or two (depending how you count, see issue 4.D.8). Anyway, the army in + Silesia has to be removed. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'ITALY', ['GRE', 'NAP']) + self.set_units(game, 'ITALY', ['F ION', 'A GRE', 'A SIL']) + self.move_to_phase(game, 'W1901A') + self.process(game) + assert self.owner_name(game, 'F ION') == 'ITALY' + assert self.owner_name(game, 'A GRE') == 'ITALY' + assert self.owner_name(game, 'A SIL') is None + + def test_6_j_11(self): + """ 6.J.11. TEST CASE, CIVIL DISORDER COUNTING DISTANCE WITHOUT CONVOYING FLEET + If there is no convoying fleet the result depends on the interpretation of the rules. + Italy has to remove one. + Italy has armies in Greece and Silesia. + Italy does not order a disband. + The distance from Greece to one of the Italian home supply centers is one, two or three (depending how + you count, see issue 4.D.8). + I prefer that sea areas just add one to the distance. According to this preference, the distance is two + and the army in Silesia has to be removed. + """ + game = self.create_game() + self.clear_units(game) + self.clear_centers(game) + self.set_centers(game, 'ITALY', 'GRE') + self.set_units(game, 'ITALY', ['A GRE', 'A SIL']) + self.move_to_phase(game, 'W1901A') + self.process(game) + assert self.owner_name(game, 'A GRE') == 'ITALY' + assert self.owner_name(game, 'A SIL') is None + +def check_dislodged(game, unit, dislodger): + """ Checks if a unit has been dislodged """ + if not game: + return False + if unit not in game.dislodged: + return False + return game.dislodged[unit] == ''.join(dislodger.split()[1:])[:3] diff --git a/diplomacy/tests/test_datc_no_check.py b/diplomacy/tests/test_datc_no_check.py new file mode 100644 index 0000000..59e5419 --- /dev/null +++ b/diplomacy/tests/test_datc_no_check.py @@ -0,0 +1,105 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" DATC Test Cases (Using rule NO_CHECK) + - Contains the diplomacy adjudication test cases (without order validation) +""" +from diplomacy.engine.game import Game +from diplomacy.tests.test_datc import TestDATC as RootDATC + +# ----------------- +# DATC TEST CASES (Without order validation) +# ----------------- +class TestDATCNoCheck(RootDATC): + """ DATC test cases""" + + @staticmethod + def create_game(): + """ Creates a game object""" + game = Game() + game.add_rule('NO_CHECK') + return game + + @staticmethod + def check_results(game, unit, value, phase='M'): + """ Checks adjudication results """ + # pylint: disable=too-many-return-statements + if not game: + return False + + result = game.result_history.last_value() + + # Checking if the results contain duplicate values + unit_result = result.get(unit, []) + if len(unit_result) != len(set(unit_result)): + raise RuntimeError('Duplicate values detected in %s' % unit_result) + + # Done self.processing a retreats phase + if phase == 'R': + if value == 'void' and 'void' in unit_result: + return True + if value == '': + success = unit not in game.popped and unit_result == [] + if not success: + print('Results: %s - Expected: []' % result.get(unit, '<Not Found>')) + return success + + success = unit in game.popped and value in unit_result + if not success: + print('Results: %s - Expected: %s' % (result.get(unit, '<Not Found>'), value)) + return success + + # Done self.processing a retreats phase + if phase == 'A': + if value == 'void' and 'void' in unit_result: + return True + success = value == unit_result + if not success: + print('Results: %s - Expected: %s' % (result.get(unit, '<Not Found>'), value)) + return success + + order_status = game.get_order_status(unit=unit) + + # >>>>>>>>>>>>>>>>>>>>>>>> + # For NO_CHECK, we expect to find the unit in game.orderable_units + # But we require that the order is marked as 'void' + # As opposed to a regular game, where an invalid order is dropped + # <<<<<<<<<<<<<<<<<<<<<<<< + # Invalid order + if value == 'void': + if 'void' in result.get(unit, []): + return True + return False + + # Invalid unit + if unit not in game.command: + print('Results: %s NOT FOUND - Expected: %s' % (unit, value)) + return False + + # Expected no errors + if value == '': + if order_status: + print('Results: %s - Expected: []' % order_status) + return False + return True + + # Incorrect error + if value not in game.get_order_status(unit=unit): + print('Results: %s - Expected: %s' % (order_status, value)) + return False + + # Correct value + return True diff --git a/diplomacy/tests/test_datc_no_expand.py b/diplomacy/tests/test_datc_no_expand.py new file mode 100644 index 0000000..ee156f4 --- /dev/null +++ b/diplomacy/tests/test_datc_no_expand.py @@ -0,0 +1,97 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" DATC Test Cases (No Expansion) + - Contains the diplomacy adjudication test cases (without order expansion) +""" +from diplomacy.tests.test_datc import TestDATC as RootDATC + +# ----------------- +# DATC TEST CASES (Without order expansion) +# ----------------- +class TestDATCNoExpand(RootDATC): + """ DATC test cases""" + + @staticmethod + def set_orders(game, power_name, orders): + """ Submit orders """ + game.set_orders(power_name, orders, expand=False) + + def test_6_b_2(self): + """ 6.B.2. TEST CASE, MOVING WITH UNSPECIFIED COAST WHEN COAST IS NOT NECESSARY + There is only one coast possible in this case: + France: F Gascony - Spain + Since the North Coast is the only coast that can be reached, it seems logical that + the a move is attempted to the north coast of Spain. Some adjudicators require that a coast + is also specified in this case and will decide that the move fails or take a default coast (see 4.B.2). + I prefer that an attempt is made to the only possible coast, the north coast of Spain. + """ + # Expected to failed + pass + + def test_6_b_9(self): + """ 6.B.9. TEST CASE, SUPPORTING WITH WRONG COAST + Coasts can be specified in a support, but the result depends on the house rules. + France: F Portugal Supports F Mid-Atlantic Ocean - Spain(nc) + France: F Mid-Atlantic Ocean - Spain(sc) + Italy: F Gulf of Lyon Supports F Western Mediterranean - Spain(sc) + Italy: F Western Mediterranean - Spain(sc) + See issue 4.B.4. If it is required that the coast matches, then the support of the French fleet in the + Mid-Atlantic Ocean fails and that the Italian fleet in the Western Mediterranean moves successfully. Some + adjudicators ignores the coasts in support orders. In that case, the move of the Italian fleet bounces. + I prefer that the support fails and that the Italian fleet in the Western Mediterranean moves successfully. + """ + game = self.create_game() + self.clear_units(game) + self.set_units(game, 'FRANCE', ['F POR', 'F MAO']) + self.set_units(game, 'ITALY', ['F LYO', 'F WES']) + self.set_orders(game, 'FRANCE', ['F POR S F MAO - SPA/NC', 'F MAO - SPA/SC']) + self.set_orders(game, 'ITALY', ['F LYO S F WES - SPA/SC', 'F WES - SPA/SC']) + self.process(game) + assert self.check_results(game, 'F POR', 'void') + assert self.check_results(game, 'F MAO', 'bounce') + assert self.check_results(game, 'F LYO', '') + assert self.check_results(game, 'F WES', '') + assert self.owner_name(game, 'F POR') == 'FRANCE' + assert self.owner_name(game, 'F MAO') == 'FRANCE' + assert self.owner_name(game, 'F SPA') == 'ITALY' + assert self.owner_name(game, 'F SPA/NC') is None + assert self.owner_name(game, 'F SPA/SC') == 'ITALY' + assert self.owner_name(game, 'F LYO') == 'ITALY' + assert self.owner_name(game, 'F WES') is None + + def test_6_b_10(self): + """ 6.B.10. TEST CASE, UNIT ORDERED WITH WRONG COAST + A player might specify the wrong coast for the ordered unit. + France has a fleet on the south coast of Spain and orders: + France: F Spain(nc) - Gulf of Lyon + If only perfect orders are accepted, then the move will fail, but since the coast for the ordered unit + has no purpose, it might also be ignored (see issue 4.B.5). + I prefer that a move will be attempted. + """ + # Expected to fail + pass + + def test_6_b_12(self): + """ 6.B.12. TEST CASE, ARMY MOVEMENT WITH COASTAL SPECIFICATION + For armies the coasts are irrelevant: + France: A Gascony - Spain(nc) + If only perfect orders are accepted, then the move will fail. But it is also possible that coasts are + ignored in this case and a move will be attempted (see issue 4.B.6). + I prefer that a move will be attempted. + """ + # Expected to fail + pass diff --git a/diplomacy/tests/test_game.py b/diplomacy/tests/test_game.py new file mode 100644 index 0000000..76ba146 --- /dev/null +++ b/diplomacy/tests/test_game.py @@ -0,0 +1,650 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Test_game + - Contains tests for the game object +""" +from copy import deepcopy +from diplomacy.engine.game import Game + +def test_is_game_done(): + """ Tests if the game is done """ + game = Game() + assert not game.is_game_done + game.phase = 'COMPLETED' + assert game.is_game_done + +def test_create_game(): + """ Test - Creates a game """ + game = Game() + assert not game.error + +def test_get_units(): + """ Tests - get units """ + game = Game() + game.clear_units() + game.set_units('FRANCE', ['A PAR', 'A MAR']) + game.set_units('ENGLAND', ['A PAR', 'A LON']) + units = game.get_units() + assert units['AUSTRIA'] == [] + assert units['ENGLAND'] == ['A PAR', 'A LON'] + assert units['FRANCE'] == ['A MAR'] + assert units['GERMANY'] == [] + assert units['ITALY'] == [] + assert units['RUSSIA'] == [] + assert units['TURKEY'] == [] + + assert game.get_units('AUSTRIA') == [] + assert game.get_units('ENGLAND') == ['A PAR', 'A LON'] + assert game.get_units('FRANCE') == ['A MAR'] + assert game.get_units('GERMANY') == [] + assert game.get_units('ITALY') == [] + assert game.get_units('RUSSIA') == [] + assert game.get_units('TURKEY') == [] + + # Making sure we got a copy, and not a direct game reference + game.set_units('FRANCE', ['F MAR']) + units_2 = game.get_units() + assert units['FRANCE'] == ['A MAR'] + assert units_2['FRANCE'] == ['F MAR'] + +def test_get_centers(): + """ Test - get centers """ + game = Game() + centers = game.get_centers() + assert centers['AUSTRIA'] == ['BUD', 'TRI', 'VIE'] + assert centers['ENGLAND'] == ['EDI', 'LON', 'LVP'] + assert centers['FRANCE'] == ['BRE', 'MAR', 'PAR'] + assert centers['GERMANY'] == ['BER', 'KIE', 'MUN'] + assert centers['ITALY'] == ['NAP', 'ROM', 'VEN'] + assert centers['RUSSIA'] == ['MOS', 'SEV', 'STP', 'WAR'] + assert centers['TURKEY'] == ['ANK', 'CON', 'SMY'] + + assert game.get_centers('AUSTRIA') == ['BUD', 'TRI', 'VIE'] + assert game.get_centers('ENGLAND') == ['EDI', 'LON', 'LVP'] + assert game.get_centers('FRANCE') == ['BRE', 'MAR', 'PAR'] + assert game.get_centers('GERMANY') == ['BER', 'KIE', 'MUN'] + assert game.get_centers('ITALY') == ['NAP', 'ROM', 'VEN'] + assert game.get_centers('RUSSIA') == ['MOS', 'SEV', 'STP', 'WAR'] + assert game.get_centers('TURKEY') == ['ANK', 'CON', 'SMY'] + + # Making sure we got a copy, and not a direct game reference + austria = game.get_power('AUSTRIA') + austria.centers.remove('BUD') + centers_2 = game.get_centers() + assert centers['AUSTRIA'] == ['BUD', 'TRI', 'VIE'] + assert centers_2['AUSTRIA'] == ['TRI', 'VIE'] + +def test_get_orders(): + """ Test - get orders """ + check_sorted = lambda list_1, list_2: sorted(list_1) == sorted(list_2) + game = Game() + + # Movement phase + game.set_orders('FRANCE', ['A PAR H', 'A MAR - BUR']) + game.set_orders('ENGLAND', ['LON H']) + orders = game.get_orders() + assert check_sorted(orders['AUSTRIA'], []) + assert check_sorted(orders['ENGLAND'], ['F LON H']) + assert check_sorted(orders['FRANCE'], ['A PAR H', 'A MAR - BUR']) + assert check_sorted(orders['GERMANY'], []) + assert check_sorted(orders['ITALY'], []) + assert check_sorted(orders['RUSSIA'], []) + assert check_sorted(orders['TURKEY'], []) + + assert check_sorted(game.get_orders('AUSTRIA'), []) + assert check_sorted(game.get_orders('ENGLAND'), ['F LON H']) + assert check_sorted(game.get_orders('FRANCE'), ['A PAR H', 'A MAR - BUR']) + assert check_sorted(game.get_orders('GERMANY'), []) + assert check_sorted(game.get_orders('ITALY'), []) + assert check_sorted(game.get_orders('RUSSIA'), []) + assert check_sorted(game.get_orders('TURKEY'), []) + + # Making sure we got a copy, and not a direct game reference + france = game.get_power('FRANCE') + del france.orders['A PAR'] + orders_2 = game.get_orders() + assert check_sorted(orders['FRANCE'], ['A PAR H', 'A MAR - BUR']) + assert check_sorted(orders_2['FRANCE'], ['A MAR - BUR']) + + # Moving to W1901A + game.clear_units('FRANCE') + game.set_centers('FRANCE', 'SPA') + game.process() + game.process() + assert game.get_current_phase() == 'W1901A' + + # Adjustment phase + game.set_orders('FRANCE', ['A MAR B', 'F MAR B']) + game.set_orders('AUSTRIA', 'A PAR H') + orders = game.get_orders() + assert check_sorted(orders['AUSTRIA'], []) + assert check_sorted(orders['ENGLAND'], []) + assert check_sorted(orders['FRANCE'], ['A MAR B']) + assert check_sorted(orders['GERMANY'], []) + assert check_sorted(orders['ITALY'], []) + assert check_sorted(orders['RUSSIA'], []) + assert check_sorted(orders['TURKEY'], []) + + assert check_sorted(game.get_orders('AUSTRIA'), []) + assert check_sorted(game.get_orders('ENGLAND'), []) + assert check_sorted(game.get_orders('FRANCE'), ['A MAR B']) + assert check_sorted(game.get_orders('GERMANY'), []) + assert check_sorted(game.get_orders('ITALY'), []) + assert check_sorted(game.get_orders('RUSSIA'), []) + assert check_sorted(game.get_orders('TURKEY'), []) + +def test_get_orders_no_check(): + """ Test - get orders NO_CHECK """ + check_sorted = lambda list_1, list_2: sorted(list_1) == sorted(list_2) + game = Game() + game.add_rule('NO_CHECK') + + # Movement phase + game.set_orders('FRANCE', ['A PAR H', 'A MAR - BUR']) + game.set_orders('ENGLAND', ['LON H']) + orders = game.get_orders() + assert check_sorted(orders['AUSTRIA'], []) + assert check_sorted(orders['ENGLAND'], ['LON H']) # Should not be fixed + assert check_sorted(orders['FRANCE'], ['A PAR H', 'A MAR - BUR']) + assert check_sorted(orders['GERMANY'], []) + assert check_sorted(orders['ITALY'], []) + assert check_sorted(orders['RUSSIA'], []) + assert check_sorted(orders['TURKEY'], []) + + assert check_sorted(game.get_orders('AUSTRIA'), []) + assert check_sorted(game.get_orders('ENGLAND'), ['LON H']) # Should not be fixed + assert check_sorted(game.get_orders('FRANCE'), ['A PAR H', 'A MAR - BUR']) + assert check_sorted(game.get_orders('GERMANY'), []) + assert check_sorted(game.get_orders('ITALY'), []) + assert check_sorted(game.get_orders('RUSSIA'), []) + assert check_sorted(game.get_orders('TURKEY'), []) + + # Making sure we got a copy, and not a direct game reference + france = game.get_power('FRANCE') + france.orders = {order_ix: order_value for order_ix, order_value in france.orders.items() + if not order_value.startswith('A PAR')} + orders_2 = game.get_orders() + assert check_sorted(orders['FRANCE'], ['A PAR H', 'A MAR - BUR']) + assert check_sorted(orders_2['FRANCE'], ['A MAR - BUR']) + + # Moving to W1901A + game.clear_units('FRANCE') + game.set_centers('FRANCE', 'SPA') + game.process() + game.process() + assert game.get_current_phase() == 'W1901A' + + # Adjustment phase + game.set_orders('FRANCE', ['A MAR B', 'F MAR B']) + game.set_orders('AUSTRIA', 'A PAR H') + orders = game.get_orders() + assert check_sorted(orders['AUSTRIA'], []) # 'A PAR H' is VOID + assert check_sorted(orders['ENGLAND'], []) + assert check_sorted(orders['FRANCE'], ['A MAR B']) # 'F MAR B' is VOID + assert check_sorted(orders['GERMANY'], []) + assert check_sorted(orders['ITALY'], []) + assert check_sorted(orders['RUSSIA'], []) + assert check_sorted(orders['TURKEY'], []) + + assert check_sorted(game.get_orders('AUSTRIA'), []) + assert check_sorted(game.get_orders('ENGLAND'), []) + assert check_sorted(game.get_orders('FRANCE'), ['A MAR B']) + assert check_sorted(game.get_orders('GERMANY'), []) + assert check_sorted(game.get_orders('ITALY'), []) + assert check_sorted(game.get_orders('RUSSIA'), []) + assert check_sorted(game.get_orders('TURKEY'), []) + +def test_get_order_status(): + """ Tests - get order status """ + game = Game() + game.clear_units() + game.set_units('ITALY', 'A VEN') + game.set_units('AUSTRIA', 'A VIE') + game.set_orders('ITALY', 'A VEN - TYR') + game.set_orders('AUSTRIA', 'A VIE - TYR') + game.process() + results = game.get_order_status() + assert 'bounce' in results['ITALY']['A VEN'] + assert 'bounce' in results['AUSTRIA']['A VIE'] + assert 'bounce' in game.get_order_status(unit='A VEN') + assert 'bounce' in game.get_order_status(unit='A VIE') + assert 'bounce' in game.get_order_status('ITALY')['A VEN'] + assert 'bounce' in game.get_order_status('AUSTRIA')['A VIE'] + +def test_set_units(): + """ Test - Sets units """ + game = Game() + game.clear_units() + game.set_units('FRANCE', ['A PAR', 'A MAR', '*A GAS'], reset=False) + game.set_units('ENGLAND', ['A PAR', 'A LON']) + assert game.get_power('AUSTRIA').units == [] + assert game.get_power('ENGLAND').units == ['A PAR', 'A LON'] + assert game.get_power('FRANCE').units == ['A MAR'] + assert 'A GAS' in game.get_power('FRANCE').retreats + assert game.get_power('GERMANY').units == [] + assert game.get_power('ITALY').units == [] + assert game.get_power('RUSSIA').units == [] + assert game.get_power('TURKEY').units == [] + + # Adding F PIC to England without resetting + game.set_units('ENGLAND', ['F PIC'], reset=False) + assert game.get_power('ENGLAND').units == ['A PAR', 'A LON', 'F PIC'] + + # Adding F PIC to England with resetting + game.set_units('ENGLAND', ['F PIC'], reset=True) + assert game.get_power('ENGLAND').units == ['F PIC'] + + # Adding F PAR (Illegal unit) to England without resetting + game.set_units('ENGLAND', ['F PAR'], reset=False) + assert game.get_power('ENGLAND').units == ['F PIC'] + +def test_set_centers(): + """ Tests - Sets centers """ + game = Game() + game.clear_centers() + game.set_centers('FRANCE', ['PAR', 'MAR', 'GAS']) # GAS is not a valid SC loc + game.set_centers('ENGLAND', ['PAR', 'LON']) + assert game.get_power('AUSTRIA').centers == [] + assert game.get_power('ENGLAND').centers == ['PAR', 'LON'] + assert game.get_power('FRANCE').centers == ['MAR'] + assert game.get_power('GERMANY').centers == [] + assert game.get_power('ITALY').centers == [] + assert game.get_power('RUSSIA').centers == [] + assert game.get_power('TURKEY').centers == [] + + # Adding BUD to England without resetting + game.set_centers('ENGLAND', 'BUD', reset=False) + assert game.get_power('ENGLAND').centers == ['PAR', 'LON', 'BUD'] + + # Adding BUD to England with resetting + game.set_centers('ENGLAND', ['BUD'], reset=True) + assert game.get_power('ENGLAND').centers == ['BUD'] + + # Adding UKR to England (illegal SC) + game.set_centers('ENGLAND', 'UKR', reset=False) + assert game.get_power('ENGLAND').centers == ['BUD'] + +def test_set_orders(): + """ Test - Sets orders """ + game = Game() + game.clear_units() + game.set_units('ITALY', 'A VEN') + game.set_units('AUSTRIA', 'A VIE') + game.set_units('FRANCE', 'A PAR') + game.set_orders('ITALY', 'A VEN - TYR') + game.set_orders('AUSTRIA', 'A VIE - TYR') + + game.set_orders('FRANCE', ['', '', 'A PAR - GAS', '', '', '']) + game.set_orders('RUSSIA', '') + game.set_orders('GERMANY', []) + assert game.get_orders('FRANCE') == ['A PAR - GAS'] + assert not game.get_orders('RUSSIA') + assert not game.get_orders('GERMANY') + + game.process() + results = game.get_order_status() + assert 'bounce' in results['ITALY']['A VEN'] + assert 'bounce' in results['AUSTRIA']['A VIE'] + +def test_set_orders_replace(): + """ Test - Sets orders with replace=True """ + check_sorted = lambda list_1, list_2: sorted(list_1) == sorted(list_2) + + # Regular Movement Phase + game = Game() + game.clear_units() + game.set_units('ITALY', ['A VEN', 'A PAR']) + game.set_units('AUSTRIA', 'A VIE') + game.set_orders('ITALY', ['A VEN - TYR', 'A PAR H']) + game.set_orders('AUSTRIA', 'A VIE - TYR') + game.set_orders('ITALY', 'A PAR - GAS') + orders = game.get_orders() + assert check_sorted(orders['ITALY'], ['A VEN - TYR', 'A PAR - GAS']) + assert check_sorted(orders['AUSTRIA'], ['A VIE - TYR']) + + # NO_CHECK Movement Phase + game = Game() + game.add_rule('NO_CHECK') + game.clear_units() + game.set_units('ITALY', ['A VEN', 'A PAR']) + game.set_units('AUSTRIA', 'A VIE') + game.set_orders('ITALY', ['A VEN - TYR', 'A PAR H']) + game.set_orders('AUSTRIA', 'A VIE - TYR') + game.set_orders('ITALY', 'A PAR - GAS') + orders = game.get_orders() + assert check_sorted(orders['ITALY'], ['A VEN - TYR', 'A PAR - GAS']) + assert check_sorted(orders['AUSTRIA'], ['A VIE - TYR']) + + # Regular Retreat Phase + game = Game() + game.clear_units() + game.set_units('ITALY', ['A BRE', 'A PAR']) + game.set_units('AUSTRIA', 'A GAS') + game.set_orders('ITALY', ['A BRE - GAS', 'A PAR S A BRE - GAS']) + game.set_orders('AUSTRIA', 'A GAS H') + game.process() + game.set_orders('AUSTRIA', 'A GAS R MAR') + game.set_orders('AUSTRIA', 'A GAS R SPA') + orders = game.get_orders() + assert check_sorted(orders['AUSTRIA'], ['A GAS R SPA']) + + # NO_CHECK Retreat Phase + game = Game() + game.add_rule('NO_CHECK') + game.clear_units() + game.set_units('ITALY', ['A BRE', 'A PAR']) + game.set_units('AUSTRIA', 'A GAS') + game.set_orders('ITALY', ['A BRE - GAS', 'A PAR S A BRE - GAS']) + game.set_orders('AUSTRIA', 'A GAS H') + game.process() + game.set_orders('AUSTRIA', 'A GAS R MAR') + game.set_orders('AUSTRIA', 'A GAS R SPA') + orders = game.get_orders() + assert check_sorted(orders['AUSTRIA'], ['A GAS R SPA']) + + # Regular Adjustment Phase + game = Game() + game.clear_units() + game.clear_centers() + game.set_units('FRANCE', ['A BRE', 'A PAR']) + game.set_units('AUSTRIA', 'A GAS') + game.set_orders('FRANCE', 'A PAR - PIC') + game.process() + game.set_orders('FRANCE', ['A PIC - BEL', 'A BRE - PAR']) + game.process() + game.set_orders('FRANCE', 'A BRE B') + game.set_orders('FRANCE', 'F BRE B') + orders = game.get_orders() + assert check_sorted(orders['FRANCE'], ['F BRE B']) + + # NO_CHECK Adjustment Phase + game = Game() + game.add_rule('NO_CHECK') + game.clear_units() + game.clear_centers() + game.set_units('FRANCE', ['A BRE', 'A PAR']) + game.set_units('AUSTRIA', 'A GAS') + game.set_orders('FRANCE', 'A PAR - PIC') + game.process() + game.set_orders('FRANCE', ['A PIC - BEL', 'A BRE - PAR']) + game.process() + game.set_orders('FRANCE', 'A BRE B') + game.set_orders('FRANCE', 'F BRE B') + orders = game.get_orders() + assert check_sorted(orders['FRANCE'], ['F BRE B']) + +def test_set_orders_no_replace(): + """ Test - Sets orders with replace=False """ + check_sorted = lambda list_1, list_2: sorted(list_1) == sorted(list_2) + + # Regular Movement Phase + game = Game() + game.clear_units() + game.set_units('ITALY', ['A VEN', 'A PAR']) + game.set_units('AUSTRIA', 'A VIE') + game.set_orders('ITALY', ['A VEN - TYR', 'A PAR H'], replace=False) + game.set_orders('AUSTRIA', 'A VIE - TYR', replace=False) + game.set_orders('ITALY', 'A PAR - GAS', replace=False) + orders = game.get_orders() + assert check_sorted(orders['ITALY'], ['A VEN - TYR', 'A PAR H']) + assert check_sorted(orders['AUSTRIA'], ['A VIE - TYR']) + + # NO_CHECK Movement Phase + game = Game() + game.add_rule('NO_CHECK') + game.clear_units() + game.set_units('ITALY', ['A VEN', 'A PAR']) + game.set_units('AUSTRIA', 'A VIE') + game.set_orders('ITALY', ['A VEN - TYR', 'A PAR H'], replace=False) + game.set_orders('AUSTRIA', 'A VIE - TYR', replace=False) + game.set_orders('ITALY', 'A PAR - GAS', replace=False) + orders = game.get_orders() + assert check_sorted(orders['ITALY'], ['A VEN - TYR', 'A PAR H', 'A PAR - GAS']) + assert check_sorted(orders['AUSTRIA'], ['A VIE - TYR']) + + # Regular Retreat Phase + game = Game() + game.clear_units() + game.set_units('ITALY', ['A BRE', 'A PAR']) + game.set_units('AUSTRIA', 'A GAS') + game.set_orders('ITALY', ['A BRE - GAS', 'A PAR S A BRE - GAS'], replace=False) + game.set_orders('AUSTRIA', 'A GAS H', replace=False) + game.process() + game.set_orders('AUSTRIA', 'A GAS R MAR', replace=False) + game.set_orders('AUSTRIA', 'A GAS R SPA', replace=False) + orders = game.get_orders() + assert check_sorted(orders['AUSTRIA'], ['A GAS R MAR']) + + # NO_CHECK Retreat Phase + game = Game() + game.add_rule('NO_CHECK') + game.clear_units() + game.set_units('ITALY', ['A BRE', 'A PAR']) + game.set_units('AUSTRIA', 'A GAS') + game.set_orders('ITALY', ['A BRE - GAS', 'A PAR S A BRE - GAS'], replace=False) + game.set_orders('AUSTRIA', 'A GAS H', replace=False) + game.process() + game.set_orders('AUSTRIA', 'A GAS R MAR', replace=False) + game.set_orders('AUSTRIA', 'A GAS R SPA', replace=False) + orders = game.get_orders() + assert check_sorted(orders['AUSTRIA'], ['A GAS R MAR']) + + # Regular Adjustment Phase + game = Game() + game.clear_units() + game.clear_centers() + game.set_units('FRANCE', ['A BRE', 'A PAR']) + game.set_units('AUSTRIA', 'A GAS') + game.set_orders('FRANCE', 'A PAR - PIC', replace=False) + game.process() + game.set_orders('FRANCE', ['A PIC - BEL', 'A BRE - PAR'], replace=False) + game.process() + game.set_orders('FRANCE', 'A BRE B', replace=False) + game.set_orders('FRANCE', 'F BRE B', replace=False) + orders = game.get_orders() + assert check_sorted(orders['FRANCE'], ['A BRE B']) + + # NO_CHECK Adjustment Phase + game = Game() + game.add_rule('NO_CHECK') + game.clear_units() + game.clear_centers() + game.set_units('FRANCE', ['A BRE', 'A PAR']) + game.set_units('AUSTRIA', 'A GAS') + game.set_orders('FRANCE', 'A PAR - PIC', replace=False) + game.process() + game.set_orders('FRANCE', ['A PIC - BEL', 'A BRE - PAR'], replace=False) + game.process() + game.set_orders('FRANCE', 'A BRE B', replace=False) + game.set_orders('FRANCE', 'F BRE B', replace=False) + orders = game.get_orders() + assert check_sorted(orders['FRANCE'], ['A BRE B']) + +def test_clear_units(): + """ Tests - Clear units """ + game = Game() + game.clear_units() + for power in game.powers.values(): + assert not power.units + assert not game.error + +def test_clear_centers(): + """ Tests - Clear centers """ + game = Game() + game.clear_centers() + for power in game.powers.values(): + assert not power.centers + assert not game.error + +def test_clear_orders(): + """ Test - Clear orders""" + game = Game() + game.clear_units() + game.set_units('ITALY', 'A VEN') + game.set_units('AUSTRIA', 'A VIE') + game.set_orders('ITALY', 'A VEN - TYR') + game.set_orders('AUSTRIA', 'A VIE - TYR') + game.clear_orders() + game.process() + results = game.get_order_status() + assert results['ITALY']['A VEN'] == [] + assert results['AUSTRIA']['A VIE'] == [] + +def test_get_current_phase(): + """ Tests - get current phase """ + game = Game() + assert game.get_current_phase() == 'S1901M' + +def test_set_current_phase(): + """ Tests - set current phase""" + game = Game() + power = game.get_power('FRANCE') + power.units.remove('A PAR') + game.set_current_phase('W1901A') + assert game.get_current_phase() == 'W1901A' + assert game.phase_type == 'A' + assert 'A PAR B' in game.get_all_possible_orders('PAR') + +def test_process_game(): + """ Tests - Process game """ + game = Game() + game.clear_units() + game.set_units('ITALY', 'A VEN') + game.set_units('AUSTRIA', 'A VIE') + game.set_orders('ITALY', 'A VEN - TYR') + game.set_orders('AUSTRIA', 'A VIE - TYR') + game.process() + results = game.get_order_status() + assert 'bounce' in results['ITALY']['A VEN'] + assert 'bounce' in results['AUSTRIA']['A VIE'] + +def test_deepcopy(): + """ Tests - deepcopy """ + game = Game() + game2 = deepcopy(game) + assert game != game2 + assert game.get_hash() == game2.get_hash() + +def test_automatic_draw(): + """ Tests - draw """ + game = Game() + assert game.map.first_year == 1901 + + # fast forward 99 years with no winter + for year in range(1, 100): + game.process() + game.process() + assert int(game.get_current_phase()[1:5]) == game.map.first_year + year + assert game.is_game_done is False + + # forward 2000 year. after this year should draw + game.process() + game.process() + assert game.is_game_done is True + assert list(sorted(game.outcome)) == list( + sorted(['W2000A', 'AUSTRIA', 'ENGLAND', 'FRANCE', 'GERMANY', 'ITALY', 'RUSSIA', 'TURKEY'])) + +def test_histories(): + """ Test order_history, state_history, message_history and messages. """ + from diplomacy.server.server_game import ServerGame + from diplomacy.utils.sorted_dict import SortedDict + from diplomacy.utils import strings + game = ServerGame(status=strings.ACTIVE) + assert game.solitaire + assert not game.n_controls + assert game.is_game_active + + assert isinstance(game.messages, SortedDict) + assert isinstance(game.message_history, SortedDict) + assert isinstance(game.order_history, SortedDict) + assert isinstance(game.state_history, SortedDict) + assert not game.messages + assert not game.message_history + assert not game.order_history + assert not game.state_history + game.new_system_message('FRANCE', 'Hello France!') + game.new_system_message('GLOBAL', 'Hello World!') + game.set_orders('FRANCE', ['A PAR H']) + assert len(game.messages) == 2 + previous_phase = game.get_current_phase() + game.process() + current_phase = game.get_current_phase() + assert previous_phase != current_phase, (previous_phase, current_phase) + assert not game.messages + assert len(game.message_history) == 1 + assert len(game.order_history) == 1 + assert len(game.state_history) == 1 + game.set_orders('AUSTRIA', ['A BUD - GAL']) + game.set_orders('FRANCE', ['A PAR H']) + game.new_system_message('GLOBAL', 'New world.') + assert len(game.messages) == 1 + game.process() + assert not game.messages + assert len(game.message_history) == 2 + assert len(game.order_history) == 2 + assert len(game.state_history) == 2 + assert all((p1 == p2 == p3) for (p1, p2, p3) in zip(game.message_history.keys(), + game.order_history.keys(), + game.state_history.keys())) + + assert game.map.compare_phases(str(game.state_history.first_key()), str(game.state_history.last_key())) < 0 + + messages_phase_1 = list(game.message_history.first_value().values()) + messages_phase_2 = list(game.message_history.last_value().values()) + orders_phase_1 = game.order_history.first_value() + orders_phase_2 = game.order_history.last_value() + assert len(messages_phase_1) == 2 + assert len(messages_phase_2) == 1 + assert messages_phase_1[0].message == 'Hello France!' + assert messages_phase_1[1].message == 'Hello World!' + assert messages_phase_2[0].message == 'New world.' + assert orders_phase_1['FRANCE'] == ['A PAR H'] + assert orders_phase_2['FRANCE'] == ['A PAR H'] + assert orders_phase_2['AUSTRIA'] == ['A BUD - GAL'] + + assert all('messages' not in state for state in game.state_history.values()) + + game_to_json = game.to_dict() + game_copy = ServerGame.from_dict(game_to_json) + assert list(game.state_history.keys()) == list(game_copy.state_history.keys()) + assert list(game.message_history.keys()) == list(game_copy.message_history.keys()) + assert list(game.order_history.keys()) == list(game_copy.order_history.keys()) + # Check histories in game copy. + messages_phase_1 = list(game_copy.message_history.first_value().values()) + messages_phase_2 = list(game_copy.message_history.last_value().values()) + orders_phase_1 = game_copy.order_history.first_value() + orders_phase_2 = game_copy.order_history.last_value() + assert len(messages_phase_1) == 2 + assert len(messages_phase_2) == 1 + assert messages_phase_1[0].message == 'Hello France!' + assert messages_phase_1[1].message == 'Hello World!' + assert messages_phase_2[0].message == 'New world.' + assert orders_phase_1['FRANCE'] == ['A PAR H'] + assert orders_phase_2['FRANCE'] == ['A PAR H'] + assert orders_phase_2['AUSTRIA'] == ['A BUD - GAL'] + +def test_result_history(): + """ Test result history. """ + short_phase_name = 'S1901M' + game = Game() + game.set_orders('FRANCE', ['A PAR - BUR', 'A MAR - BUR']) + assert game.current_short_phase == short_phase_name + game.process() + assert game.current_short_phase == 'F1901M' + phase_data = game.get_phase_from_history(short_phase_name) + assert 'bounce' in phase_data.results['A PAR'] + assert 'bounce' in phase_data.results['A MAR'] diff --git a/diplomacy/tests/test_map.py b/diplomacy/tests/test_map.py new file mode 100644 index 0000000..507efae --- /dev/null +++ b/diplomacy/tests/test_map.py @@ -0,0 +1,237 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Tests cases for Map + - Contains the test cases for the map object +""" +from copy import deepcopy +from diplomacy.engine.map import Map + +def test_init(): + """ Creates a map""" + Map() + +def test_str(): + """ Tests map.__str__ """ + this_map = deepcopy(Map()) + assert str(this_map) == this_map.name + +def test_add_homes(): + """ Tests map.add_homes """ + this_map = deepcopy(Map()) + this_map.add_homes('FRANCE', 'BRE MAR PAR'.split(), reinit=1) + assert this_map.homes['FRANCE'] == ['BRE', 'MAR', 'PAR'] + this_map.add_homes('FRANCE', [], reinit=1) + assert this_map.homes['FRANCE'] == [] + +def test_drop(): + """ Tests map.drop """ + this_map = deepcopy(Map()) + this_map.drop('STP') + assert not [loc for loc in list(this_map.locs) if loc.upper().startswith('STP')] + assert not [loc_name for (loc_name, loc) in list(this_map.loc_name.items()) if loc.startswith('STP')] + assert not [alias for (alias, value) in list(this_map.aliases.items()) if value.startswith('STP')] + assert not [homes for homes in list(this_map.homes.values()) if 'STP' in homes] + assert not [units for units in list(this_map.units.values()) for unit in units if unit[2:5] == 'STP'[:3]] + assert not [center for center in list(this_map.scs) if center.upper().startswith('STP')] + assert not [p_name for (p_name, scs) in this_map.centers.items() for center in scs if center.startswith('STP')] + assert not [loc for loc, abuts in list(this_map.loc_abut.items()) for there in abuts + if loc.startswith('STP') or there.startswith('STP')] + assert not [loc for loc in list(this_map.loc_type.keys()) if loc.startswith('STP')] + +def test_compact(): + """ Tests map.compact """ + this_map = deepcopy(Map()) + assert this_map.compact('England: Fleet Western Mediterranean -> Tyrrhenian Sea. (*bounce*)') \ + == ['ENGLAND', 'F', 'WES', 'TYS', '|'] + +def test_norm_power(): + """ Tests map.norm_power """ + this_map = deepcopy(Map()) + assert this_map.norm_power('abc def. ghi/jkl!-ABC|~ (Hello)') == 'ABCDEFGHI/JKL!ABC|~(HELLO)' + +def test_norm(): + """ Tests map.norm """ + this_map = deepcopy(Map()) + assert this_map.norm('abc def. ghi/jkl!-ABC|~ (Hello)') == 'ABC DEF GHI /JKL ! ABC | ~ ( HELLO )' + +def test_vet(): + """ Tests map.vet """ + this_map = deepcopy(Map()) + assert this_map.vet(['A B']) == [('A B', 0)] + assert this_map.vet(['SPAIN/NC']) == [('SPAIN/NC', 1)] + assert this_map.vet(['SPANISH']) == [('SPANISH', 1)] + assert this_map.vet(['A']) == [('A', 2)] + assert this_map.vet(['F']) == [('F', 2)] + assert this_map.vet(['POR']) == [('POR', 3)] + assert this_map.vet(['SPA']) == [('SPA', 3)] + assert this_map.vet(['SPA/NC']) == [('SPA/NC', 4)] + assert this_map.vet(['S']) == [('S', 5)] + assert this_map.vet(['C']) == [('C', 5)] + assert this_map.vet(['H']) == [('H', 5)] + assert this_map.vet(['-']) == [('-', 6)] + assert this_map.vet(['=']) == [('=', 6)] + assert this_map.vet(['_']) == [('_', 6)] + assert this_map.vet(['|']) == [('|', 7)] + assert this_map.vet(['?']) == [('?', 7)] + assert this_map.vet(['~']) == [('~', 7)] + assert this_map.vet(['ZZZ'], strict=0) == [('ZZZ', 3)] + assert this_map.vet(['ZZZ'], strict=1) == [('ZZZ', -3)] + +def test_area_type(): + """ Tests map.area_type """ + this_map = deepcopy(Map()) + assert this_map.area_type('ADR') == 'WATER' + assert this_map.area_type('ALB') == 'COAST' + assert this_map.area_type('BUL/EC') == 'COAST' + assert this_map.area_type('BUR') == 'LAND' + assert this_map.area_type('SWI') == 'SHUT' + +def test_default_coast(): + """ Tests map.default_coast """ + this_map = deepcopy(Map()) + assert this_map.default_coast(['F', 'GRE', '-', 'BUL']) == ['F', 'GRE', '-', 'BUL/SC'] + assert this_map.default_coast(['F', 'MAO', '-', 'SPA']) == ['F', 'MAO', '-', 'SPA'] + assert this_map.default_coast(['F', 'FIN', '-', 'STP']) == ['F', 'FIN', '-', 'STP/SC'] + assert this_map.default_coast(['F', 'NAO', '-', 'MAO']) == ['F', 'NAO', '-', 'MAO'] + +def test_abuts(): + """ Tests map.abuts """ + this_map = deepcopy(Map()) + assert this_map.abuts('A', 'POR', 'S', 'SPA/NC') == 1 + assert this_map.abuts('A', 'POR', 'C', 'SPA/NC') == 0 + assert this_map.abuts('A', 'MUN', 'S', 'SWI') == 0 + assert this_map.abuts('?', 'YOR', 'S', 'LVP') == 1 + assert this_map.abuts('F', 'YOR', 'S', 'LVP') == 0 + assert this_map.abuts('A', 'YOR', 'S', 'LVP') == 1 + assert this_map.abuts('F', 'BOT', 'S', 'STP') == 1 + assert this_map.abuts('F', 'BOT', 'S', 'MOS') == 0 + assert this_map.abuts('F', 'VEN', 'S', 'TUS') == 0 + assert this_map.abuts('A', 'POR', 'C', 'MAO') == 1 + +def test_is_valid_unit(): + """ Tests maps.is_valid_unit """ + # ADR = WATER + # ALB = COAST + # BUL/EC = COAST + # BUR = LAND + # SWI = SHUT + this_map = deepcopy(Map()) + assert this_map.is_valid_unit('A ADR', no_coast_ok=0, shut_ok=0) == 0 + assert this_map.is_valid_unit('A ALB', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('A BUL', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('A BUL/EC', no_coast_ok=0, shut_ok=0) == 0 + assert this_map.is_valid_unit('A BUR', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('A SWI', no_coast_ok=0, shut_ok=0) == 0 + assert this_map.is_valid_unit('F ADR', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('F ALB', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('F BUL', no_coast_ok=0, shut_ok=0) == 0 + assert this_map.is_valid_unit('F BUL/EC', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('F BUR', no_coast_ok=0, shut_ok=0) == 0 + assert this_map.is_valid_unit('F SWI', no_coast_ok=0, shut_ok=0) == 0 + assert this_map.is_valid_unit('F ADR', no_coast_ok=1, shut_ok=0) == 1 + assert this_map.is_valid_unit('F ALB', no_coast_ok=1, shut_ok=0) == 1 + assert this_map.is_valid_unit('F BUL', no_coast_ok=1, shut_ok=0) == 1 + assert this_map.is_valid_unit('F BUL/EC', no_coast_ok=1, shut_ok=0) == 1 + assert this_map.is_valid_unit('F BUR', no_coast_ok=1, shut_ok=0) == 0 + assert this_map.is_valid_unit('F SWI', no_coast_ok=1, shut_ok=0) == 0 + assert this_map.is_valid_unit('? ADR', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('? ALB', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('? BUL', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('? BUL/EC', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('? BUR', no_coast_ok=0, shut_ok=0) == 1 + assert this_map.is_valid_unit('? SWI', no_coast_ok=0, shut_ok=0) == 0 + assert this_map.is_valid_unit('A SWI', no_coast_ok=0, shut_ok=1) == 1 + assert this_map.is_valid_unit('F SWI', no_coast_ok=0, shut_ok=1) == 1 + assert this_map.is_valid_unit('? SWI', no_coast_ok=0, shut_ok=1) == 1 + +def test_abut_list(): + """ Tests map.abut_list """ + this_map = deepcopy(Map()) + this_map.loc_abut['---'] = ['ABC', 'DEF', 'GHI'] + this_map.loc_abut['aaa'] = ['LOW', 'HIG', 'MAY'] + assert this_map.abut_list('---') == ['ABC', 'DEF', 'GHI'] + assert this_map.abut_list('AAA') == ['LOW', 'HIG', 'MAY'] + assert this_map.abut_list('LVP') == ['CLY', 'edi', 'IRI', 'NAO', 'WAL', 'yor'] + +def test_compare_phases(): + """ Tests map.compare_phases """ + this_map = deepcopy(Map()) + assert this_map.compare_phases('FORMING', 'S1901M') == -1 + assert this_map.compare_phases('COMPLETED', 'S1901M') == 1 + assert this_map.compare_phases('S1901M', 'FORMING') == 1 + assert this_map.compare_phases('S1901M', 'COMPLETED') == -1 + assert this_map.compare_phases('FORMING', 'COMPLETED') == -1 + assert this_map.compare_phases('COMPLETED', 'FORMING') == 1 + assert this_map.compare_phases('S1901M', 'S1902M') == -1 + assert this_map.compare_phases('S1902M', 'S1901M') == 1 + assert this_map.compare_phases('S1901M', 'F1901M') == -1 + assert this_map.compare_phases('F1901M', 'S1901M') == 1 + assert this_map.compare_phases('S1901?', 'S1901R') == 0 + assert this_map.compare_phases('F1901?', 'F1901R') == 0 + assert this_map.compare_phases('W1901?', 'W1901A') == 0 + +def test_find_next_phase(): + """ Tests map.find_next_phase """ + this_map = deepcopy(Map()) + assert this_map.find_next_phase('FORMING') == 'FORMING' + assert this_map.find_next_phase('COMPLETED') == 'COMPLETED' + assert this_map.find_next_phase('WINTER 1901 ADJUSTMENTS') == 'SPRING 1902 MOVEMENT' + assert this_map.find_next_phase('FALL 1901 RETREATS', phase_type='M') == 'SPRING 1902 MOVEMENT' + assert this_map.find_next_phase('SPRING 1902 RETREATS', phase_type='M', skip=1) == 'SPRING 1903 MOVEMENT' + +def test_find_previous_phase(): + """ Tests map.find_previous_phase """ + this_map = deepcopy(Map()) + assert this_map.find_previous_phase('FORMING') == 'FORMING' + assert this_map.find_previous_phase('COMPLETED') == 'COMPLETED' + assert this_map.find_previous_phase('SPRING 1902 MOVEMENT') == 'WINTER 1901 ADJUSTMENTS' + assert this_map.find_previous_phase('SPRING 1902 MOVEMENT', phase_type='R') == 'FALL 1901 RETREATS' + assert this_map.find_previous_phase('SPRING 1903 MOVEMENT', phase_type='R', skip=1) == 'SPRING 1902 RETREATS' + +def test_phase_abbr(): + """ Tests map.phase_abbr """ + this_map = deepcopy(Map()) + assert this_map.phase_abbr('SPRING 1901 MOVEMENT') == 'S1901M' + assert this_map.phase_abbr('SPRING 1901 RETREATS') == 'S1901R' + assert this_map.phase_abbr('FALL 1901 MOVEMENT') == 'F1901M' + assert this_map.phase_abbr('FALL 1901 RETREATS') == 'F1901R' + assert this_map.phase_abbr('WINTER 1901 ADJUSTMENTS') == 'W1901A' + assert this_map.phase_abbr('spring 1901 movement') == 'S1901M' + assert this_map.phase_abbr('spring 1901 retreats') == 'S1901R' + assert this_map.phase_abbr('fall 1901 movement') == 'F1901M' + assert this_map.phase_abbr('fall 1901 retreats') == 'F1901R' + assert this_map.phase_abbr('winter 1901 adjustments') == 'W1901A' + assert this_map.phase_abbr('COMPLETED') == 'COMPLETED' + assert this_map.phase_abbr('FORMING') == 'FORMING' + assert this_map.phase_abbr('Bad') == '?????' + assert this_map.phase_abbr('Bad', default='Test') == 'Test' + +def test_phase_long(): + """ Test map.phase_long """ + this_map = deepcopy(Map()) + assert this_map.phase_long('S1901M') == 'SPRING 1901 MOVEMENT' + assert this_map.phase_long('S1901R') == 'SPRING 1901 RETREATS' + assert this_map.phase_long('F1901M') == 'FALL 1901 MOVEMENT' + assert this_map.phase_long('F1901R') == 'FALL 1901 RETREATS' + assert this_map.phase_long('W1901A') == 'WINTER 1901 ADJUSTMENTS' + assert this_map.phase_long('s1901m') == 'SPRING 1901 MOVEMENT' + assert this_map.phase_long('s1901r') == 'SPRING 1901 RETREATS' + assert this_map.phase_long('f1901m') == 'FALL 1901 MOVEMENT' + assert this_map.phase_long('f1901r') == 'FALL 1901 RETREATS' + assert this_map.phase_long('w1901a') == 'WINTER 1901 ADJUSTMENTS' + assert this_map.phase_long('bad') == '?????' + assert this_map.phase_long('bad', default='Test') == 'Test' diff --git a/diplomacy/tests/test_pytest.py b/diplomacy/tests/test_pytest.py new file mode 100644 index 0000000..f3b56d5 --- /dev/null +++ b/diplomacy/tests/test_pytest.py @@ -0,0 +1,23 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Pytest checker """ + +# Check that tests are running +# Avoids pytests not having tests to run +def test_pytest(): + """ Should always pass """ + assert True diff --git a/diplomacy/utils/__init__.py b/diplomacy/utils/__init__.py new file mode 100644 index 0000000..754f9f3 --- /dev/null +++ b/diplomacy/utils/__init__.py @@ -0,0 +1,22 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Utils + - Contains various utilities for the Diplomacy engine +""" +from .keywords import KEYWORDS, ALIASES +from .priority_dict import PriorityDict +from .time import str_to_seconds, trunc_time, next_time_at diff --git a/diplomacy/utils/common.py b/diplomacy/utils/common.py new file mode 100644 index 0000000..df2897a --- /dev/null +++ b/diplomacy/utils/common.py @@ -0,0 +1,212 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Common utils symbols used in diplomacy network code. """ +import base64 +import binascii +import hashlib +import traceback +import os +import re +import sys +from datetime import datetime + +import bcrypt + +from diplomacy.utils.exceptions import CommonKeyException + +# Datetime since timestamp 0. +EPOCH = datetime.utcfromtimestamp(0) + +# Regex used for conversion from camel case to snake case. +REGEX_CONSECUTIVE_UPPER_CASES = re.compile('[A-Z]{2,}') +REGEX_LOWER_THEN_UPPER_CASES = re.compile('([a-z0-9])([A-Z])') +REGEX_UNDERSCORE_THEN_LETTER = re.compile('_([a-z])') +REGEX_START_BY_LOWERCASE = re.compile('^[a-z]') + +def _sub_hash_password(password): + """ Hash long password to allow bcrypt to handle password longer than 72 characters. Module private method. + :param password: password to hash. + :return: (String) The hashed password. + """ + # Bcrypt only handles passwords up to 72 characters. We use this hashing method as a work around. + # Suggested in bcrypt PyPI page (2018/02/08 12:36 EST): https://pypi.python.org/pypi/bcrypt/3.1.0 + return base64.b64encode(hashlib.sha256(password.encode('utf-8')).digest()) + +def is_valid_password(password, hashed): + """ Check if password matches hashed. + :param password: password to check. + :param hashed: a password hashed with method hash_password(). + :return: (Boolean). Indicates if the password matches the hash. + """ + return bcrypt.checkpw(_sub_hash_password(password), hashed.encode('utf-8')) + +def hash_password(password): + """ Hash password. Accepts password longer than 72 characters. Public method. + :param password: The password to hash + :return: (String). The hashed password. + """ + return bcrypt.hashpw(_sub_hash_password(password), bcrypt.gensalt(14)).decode('utf-8') + +def generate_token(n_bytes=128): + """ Generate a token with 2 * n_bytes characters (n_bytes bytes encoded in hexadecimal). """ + return binascii.hexlify(os.urandom(n_bytes)).decode('utf-8') + +def is_dictionary(dict_to_check): + """ Check if given variable is a dictionary-like object. + :param dict_to_check: Dictionary to check. + :return: (Boolean). Indicates if the object is a dictionary. + """ + return isinstance(dict_to_check, dict) or all( + hasattr(dict_to_check, expected_attribute) + for expected_attribute in ( + '__len__', + '__contains__', + '__bool__', + '__iter__', + '__getitem__', + 'keys', + 'values', + 'items', + ) + ) + +def is_sequence(seq_to_check): + """ Check if given variable is a sequence-like object. + Note that strings and dictionary-like objects will not be considered as sequences. + :param seq_to_check: Sequence-like object to check. + :return: (Boolean). Indicates if the object is sequence-like. + """ + # Strings and dicts are not valid sequences. + if isinstance(seq_to_check, str) or is_dictionary(seq_to_check): + return False + return hasattr(seq_to_check, '__iter__') + +def camel_case_to_snake_case(name): + """ Convert a string (expected to be in camel case) to snake case. + :param name: string to convert. + :return: string: snake case version of given name. + """ + if name == '': + return name + separated_consecutive_uppers = REGEX_CONSECUTIVE_UPPER_CASES.sub(lambda m: '_'.join(c for c in m.group(0)), name) + return REGEX_LOWER_THEN_UPPER_CASES.sub(r'\1_\2', separated_consecutive_uppers).lower() + +def snake_case_to_upper_camel_case(name): + """ Convert a string (expected to be in snake case) to camel case and convert first letter to upper case + if it's in lowercase. + :param name: string to convert. + :return: camel case version of given name. + """ + if name == '': + return name + first_lower_case_to_upper = REGEX_START_BY_LOWERCASE.sub(lambda m: m.group(0).upper(), name) + return REGEX_UNDERSCORE_THEN_LETTER.sub(lambda m: m.group(1).upper(), first_lower_case_to_upper) + +def assert_no_common_keys(dict1, dict2): + """ Check that dictionaries does not share keys. + :param dict1: dict + :param dict2: dict + """ + if len(dict1) < len(dict2): + smallest_dict, biggest_dict = dict1, dict2 + else: + smallest_dict, biggest_dict = dict2, dict1 + for key in smallest_dict: + if key in biggest_dict: + raise CommonKeyException(key) + +def timestamp_microseconds(): + """ Return current timestamp with microsecond resolution. + :return: int + """ + delta = datetime.now() - EPOCH + return (delta.days * 24 * 60 * 60 + delta.seconds) * 1000000 + delta.microseconds + +def str_cmp_class(compare_function): + """ Return a new class to be used as string comparator. + + Example: + ``` + def my_cmp_func(a, b): + # a and b are two strings to compare with a specific code. + # Return -1 if a < b, 0 if a == b, 1 otherwise. + + my_class = str_cmp_class(my_cmp_func) + wrapped_str_1 = my_class(str_to_compare_1) + wrapped_str_2 = my_class(str_to_compare_2) + my_list = [wrapped_str_1, wrapped_str_2] + + # my_list will be sorted according to my_cmp_func. + my_list.sort() + ``` + + :param compare_function: a callable that takes 2 strings a and b, and compares it according to custom rules. + This function should return: + -1 (or a negative value) if a < b + 0 if a == b + 1 (or a positive value) if a > b + + :return: a comparator class, instanciable with a string. + """ + + class StringComparator: + """ A comparable wrapper class around strings. """ + + def __init__(self, value): + """ Initialize comparator with a value. Expected a string value. """ + self.value = str(value) + self.cmp_fn = compare_function + + def __str__(self): + return self.value + + def __repr__(self): + return repr(self.value) + + def __hash__(self): + return hash(self.value) + + def __eq__(self, other): + return self.cmp_fn(self.value, str(other)) == 0 + + def __lt__(self, other): + return self.cmp_fn(self.value, str(other)) < 0 + StringComparator.__name__ = 'StringComparator%s' % (id(compare_function)) + return StringComparator + +class Tornado(): + """ Utilities for Tornado. """ + + @staticmethod + def stop_loop_on_callback_error(io_loop): + """ Modify exception handler method of given IO loop so that IO loop stops and raises + as soon as an exception is thrown from a callback. + :param io_loop: IO loop + :type io_loop: tornado.ioloop.IOLoop + """ + + def new_cb_exception_handler(callback): + """ Callback exception handler used to replace IO loop default exception handler. """ + #pylint: disable=unused-argument + _, exc_value, _ = sys.exc_info() + io_loop.stop() + traceback.print_tb(exc_value.__traceback__) + print(type(exc_value).__name__) + print(exc_value) + exit(-1) + + io_loop.handle_callback_exception = new_cb_exception_handler diff --git a/diplomacy/utils/constants.py b/diplomacy/utils/constants.py new file mode 100644 index 0000000..d929e33 --- /dev/null +++ b/diplomacy/utils/constants.py @@ -0,0 +1,58 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Some constant / config values used in Diplomacy package. """ + +# Number of times to try to connect before throwing an exception. +NB_CONNECTION_ATTEMPTS = 12 + +# Time to wait between to connection trials. +ATTEMPT_DELAY_SECONDS = 5 + +# Time to wait between to server backups. +DEFAULT_BACKUP_DELAY_SECONDS = 10 * 60 # 10 minutes. + +# Default server ping interval. # Used for sockets ping. +DEFAULT_PING_SECONDS = 30 + +# Time to wait to receive a response for a request sent to server. +REQUEST_TIMEOUT_SECONDS = 30 + +# Default host name for a server to connect to. +DEFAULT_HOST = 'localhost' + +# Default port for normal non-securized server. +DEFAULT_PORT = 8432 + +# Default port for secure SSL server (not yet used). +DEFAULT_SSL_PORT = 8433 + +# Special username and password to use to connect as a bot recognized by diplomacy module. +# This bot is called "private bot". +PRIVATE_BOT_USERNAME = '#bot@2e723r43tr70fh2239-qf3947-3449-21128-9dh1321d12dm13d83820d28-9dm,xw201=ed283994f4n832483' +PRIVATE_BOT_PASSWORD = '#bot:password:28131821--mx1fh5g7hg5gg5g´[],s222222223djdjje399333x93901deedd|e[[[]{{|@S{@244f' + +# Time to wait to let a bot set orders for a dummy power. +PRIVATE_BOT_TIMEOUT_SECONDS = 60 + + +class OrderSettings: + """ Constants to define flags for attribute Power.order_is_set. """ + #pylint:disable=too-few-public-methods + ORDER_NOT_SET = 0 + ORDER_SET_EMPTY = 1 + ORDER_SET = 2 + ALL_SETTINGS = {ORDER_NOT_SET, ORDER_SET_EMPTY, ORDER_SET} diff --git a/diplomacy/utils/convoy_paths.py b/diplomacy/utils/convoy_paths.py new file mode 100644 index 0000000..f882e25 --- /dev/null +++ b/diplomacy/utils/convoy_paths.py @@ -0,0 +1,223 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Convoy paths + - Contains utilities to generate all the possible convoy paths for a given map +""" +import collections +import hashlib +import glob +import pickle +import multiprocessing +import os +from queue import Queue +import threading +import tqdm +from diplomacy.engine.map import Map +from diplomacy import settings + +# Using `os.path.expanduser()` to find home directory in a more cross-platform way. +HOME_DIRECTORY = os.path.expanduser('~') +if HOME_DIRECTORY == '~': + raise RuntimeError('Cannot find home directory. Unable to save cache') + +# Constants +__VERSION__ = '20180307_0955' + +# We need to cap convoy length, otherwise the problem gets exponential +SMALL_MAPS = ['standard', 'standard_france_austria', 'standard_germany_italy', 'ancmed', 'colonial', 'modern'] +SMALL_MAPS_CONVOY_LENGTH = 25 +ALL_MAPS_CONVOY_LENGTH = 12 +CACHE_FILE_NAME = 'convoy_paths_cache.pkl' +DISK_CACHE_PATH = os.path.join(HOME_DIRECTORY, '.cache', 'diplomacy', CACHE_FILE_NAME) + +def display_progress_bar(queue, max_loop_iters): + """ Displays a progress bar + :param queue: Multiprocessing queue to display the progress bar + :param max_loop_iters: The expected maximum number of iterations + """ + progress_bar = tqdm.tqdm(total=max_loop_iters) + for _ in iter(queue.get, None): + progress_bar.update() + progress_bar.close() + +def get_convoy_paths(map_object, start_location, max_convoy_length, queue): + """ Returns a list of possible convoy destinations with the required units to get there + Does a breadth first search from the starting location + + :param map_object: The instantiated map + :param start_location: The start location of the unit (e.g. 'LON') + :param max_convoy_length: The maximum convoy length permitted + :param queue: Multiprocessing queue to display the progress bar + :return: A list of ({req. fleets}, {reachable destinations}) + :type map_object: diplomacy.Map + """ + to_check = Queue() # Items in queue have format ({fleets location}, last fleet location) + dest_paths = {} # Dict with dest as key and a list of all paths from start_location to dest as value + + # We need to start on a coast / port + if map_object.area_type(start_location) not in ('COAST', 'PORT') or '/' in start_location: + return [] + + # Queuing all adjacent water locations from start + for loc in [loc.upper() for loc in map_object.abut_list(start_location, incl_no_coast=True)]: + if map_object.area_type(loc) in ['WATER', 'PORT']: + to_check.put(({loc}, loc)) + + # Checking all subsequent adjacencies until no more adjacencies are possible + while not to_check.empty(): + fleets_loc, last_loc = to_check.get() + + # Checking adjacencies + for loc in [loc.upper() for loc in map_object.abut_list(last_loc, incl_no_coast=True)]: + + # If we find adjacent coasts, we mark them as a possible result + if map_object.area_type(loc) in ('COAST', 'PORT') and '/' not in loc and loc != start_location: + dest_paths.setdefault(loc, []) + + # If we already have a working path that is a subset of the current fleets, we can skip + # Otherwise, we add the new path as a valid path to dest + for path in dest_paths[loc]: + if path.issubset(fleets_loc): + break + else: + dest_paths[loc] += [fleets_loc] + + # If we find adjacent water/port, we add them to the queue + elif map_object.area_type(loc) in ('WATER', 'PORT') \ + and loc not in fleets_loc \ + and len(fleets_loc) < max_convoy_length: + to_check.put((fleets_loc | {loc}, loc)) + + # Merging destinations with similar paths + similar_paths = {} + for dest, paths in dest_paths.items(): + for path in paths: + tuple_path = tuple(sorted(path)) + similar_paths.setdefault(tuple_path, set([])) + similar_paths[tuple_path] |= {dest} + + # Converting to list + results = [] + for fleets, dests in similar_paths.items(): + results += [(start_location, set(fleets), dests)] + + # Returning + queue.put(1) + return results + +def build_convoy_paths_cache(map_object, max_convoy_length): + """ Builds the convoy paths cache for a map + :param map_object: The instantiated map object + :param max_convoy_length: The maximum convoy length permitted + :return: A dictionary where the key is the number of fleets in the path and + the value is a list of convoy paths (start loc, {fleets}, {dest}) of that length for the map + :type map_object: diplomacy.Map + """ + print('Generating convoy paths for {}'.format(map_object.name)) + coasts = [loc.upper() for loc in map_object.locs + if map_object.area_type(loc) in ('COAST', 'PORT') if '/' not in loc] + + # Starts the progress bar loop + manager = multiprocessing.Manager() + queue = manager.Queue() + progress_bar = threading.Thread(target=display_progress_bar, args=(queue, len(coasts))) + progress_bar.start() + + # Getting all paths for each coasts in parallel + pool = multiprocessing.Pool(multiprocessing.cpu_count()) + tasks = [(map_object, coast, max_convoy_length, queue) for coast in coasts] + results = pool.starmap(get_convoy_paths, tasks) + pool.close() + results = [item for sublist in results for item in sublist] + queue.put(None) + progress_bar.join() + + # Splitting into buckets + buckets = collections.OrderedDict({i: [] for i in range(1, len(map_object.locs) + 1)}) + for start, fleets, dests in results: + buckets[len(fleets)] += [(start, fleets, dests)] + + # Returning + print('Found {} convoy paths for {}\n'.format(len(results), map_object.name)) + return buckets + +def get_file_md5(file_path): + """ Calculates a file MD5 hash + :param file_path: The file path + :return: The computed md5 hash + """ + hash_md5 = hashlib.md5() + with open(file_path, 'rb') as file: + for chunk in iter(lambda: file.read(4096), b''): + hash_md5.update(chunk) + return hash_md5.hexdigest() + +def add_to_cache(map_name): + """ Lazy generates convoys paths for a map and adds it to the disk cache + :param map_name: The name of the map + :return: The convoy_paths for that map + """ + disk_convoy_paths = {'__version__': __VERSION__} # Uses hash as key + + # Loading cache from disk (only if it's the correct version) + if os.path.exists(DISK_CACHE_PATH): + cache_data = pickle.load(open(DISK_CACHE_PATH, 'rb')) + if cache_data.get('__version__', '') != __VERSION__: + print('Upgrading cache from version "%s" to "%s"' % (cache_data.get('__version__', '<N/A>'), __VERSION__)) + else: + disk_convoy_paths.update(cache_data) + + # Getting map MD5 hash + map_path = os.path.join(settings.PACKAGE_DIR, 'maps', map_name + '.map') + if not os.path.exists(map_path): + return None + map_hash = get_file_md5(map_path) + + # Determining the depth of the search (small maps can have larger depth) + max_convoy_length = SMALL_MAPS_CONVOY_LENGTH if map_name in SMALL_MAPS else ALL_MAPS_CONVOY_LENGTH + + # Generating and adding to alternate cache paths + if map_hash not in disk_convoy_paths: + map_object = Map(map_name, use_cache=False) + disk_convoy_paths[map_hash] = build_convoy_paths_cache(map_object, max_convoy_length) + os.makedirs(os.path.dirname(DISK_CACHE_PATH), exist_ok=True) + pickle.dump(disk_convoy_paths, open(DISK_CACHE_PATH, 'wb')) + + # Returning + return disk_convoy_paths[map_hash] + +def get_convoy_paths_cache(): + """ Returns the current cache from disk """ + disk_convoy_paths = {} # Uses hash as key + cache_convoy_paths = {} # Use map name as key + + # Loading cache from disk (only if it's the correct version) + if os.path.exists(DISK_CACHE_PATH): + cache_data = pickle.load(open(DISK_CACHE_PATH, 'rb')) + if cache_data.get('__version__', '') == __VERSION__: + disk_convoy_paths.update(cache_data) + + # Getting map name and file paths + files_path = glob.glob(settings.PACKAGE_DIR + '/maps/*.map') + for file_path in files_path: + map_name = file_path.replace(settings.PACKAGE_DIR + '/maps/', '').replace('.map', '') + map_hash = get_file_md5(file_path) + if map_hash in disk_convoy_paths: + cache_convoy_paths[map_name] = disk_convoy_paths[map_hash] + + # Returning + return cache_convoy_paths diff --git a/diplomacy/utils/errors.py b/diplomacy/utils/errors.py new file mode 100644 index 0000000..b95da38 --- /dev/null +++ b/diplomacy/utils/errors.py @@ -0,0 +1,128 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Error + - Contains the error messages used by the engine +""" +MAP_LEAST_TWO_POWERS = 'MAP DOES NOT SPECIFY AT LEAST TWO POWERS' +MAP_LOC_NOT_FOUND = 'NAMED LOCATION NOT ON MAP: %s' +MAP_SITE_ABUTS_TWICE = 'SITES ABUT TWICE %s-%s' +MAP_NO_FULL_NAME = 'MAP LOCATION HAS NO FULL NAME: %s' +MAP_ONE_WAY_ADJ = 'ONE-WAY ADJACENCY IN MAP: %s -> %s' +MAP_BAD_HOME = 'BAD HOME FOR %s: %s' +MAP_BAD_INITIAL_OWN_CENTER = 'BAD INITIAL OWNED CENTER FOR %s: %s' +MAP_BAD_INITIAL_UNITS = 'BAD INITIAL UNIT FOR %s: %s' +MAP_CENTER_MULT_OWNED = 'CENTER MULTIPLY OWNED: %s' +MAP_BAD_PHASE = 'BAD PHASE IN MAP FILE: %s' +MAP_FILE_NOT_FOUND = 'MAP FILE NOT FOUND: %s' +MAP_BAD_VICTORY_LINE = 'BAD VICTORY LINE IN MAP FILE' +MAP_BAD_ROOT_MAP_LINE = 'BAD ROOT MAP LINE' +MAP_TWO_ROOT_MAPS = 'TWO ROOT MAPS' +MAP_FILE_MULT_USED = 'FILE MULTIPLY USED: %s' +MAP_BAD_ALIASES_IN_FILE = 'BAD ALIASES IN MAP FILE: %s' +MAP_RENAME_NOT_SUPPORTED = 'THE RENAME PLACE OPERATOR -> IS NO LONGER SUPPORTED.' +MAP_BAD_RENAME_DIRECTIVE = 'BAD RENAME DIRECTIVE: %s' +MAP_INVALID_LOC_ABBREV = 'INVALID LOCATION ABBREVIATION: %s' +MAP_LOC_RESERVED_KEYWORD = 'MAP LOCATION IS RESERVED KEYWORD: %s' +MAP_DUP_LOC_OR_POWER = 'DUPLICATE MAP LOCATION OR POWER: %s' +MAP_DUP_ALIAS_OR_POWER = 'DUPLICATE MAP ALIAS OR POWER: %s' +MAP_OWNS_BEFORE_POWER = '%s BEFORE POWER: %s' +MAP_INHABITS_BEFORE_POWER = 'INHABITS BEFORE POWER: %s' +MAP_HOME_BEFORE_POWER = '%s BEFORE POWER: %s' +MAP_UNITS_BEFORE_POWER = 'UNITS BEFORE POWER' +MAP_UNIT_BEFORE_POWER = 'UNIT BEFORE POWER: %s' +MAP_INVALID_UNIT = 'INVALID UNIT: %s' +MAP_DUMMY_REQ_LIST_POWERS = 'DUMMIES REQUIRES LIST OF POWERS' +MAP_DUMMY_BEFORE_POWER = 'DUMMY BEFORE POWER' +MAP_NO_EXCEPT_AFTER_DUMMY_ALL = 'NO EXCEPT AFTER %s ALL' +MAP_NO_POWER_AFTER_DUMMY_ALL_EXCEPT = 'NO POWER AFTER %s ALL EXCEPT' +MAP_NO_DATA_TO_AMEND_FOR = 'NO DATA TO "AMEND" FOR %s' +MAP_NO_ABUTS_FOR = 'NO "ABUTS" FOR %s' +MAP_UNPLAYED_BEFORE_POWER = 'UNPLAYED BEFORE POWER' +MAP_NO_EXCEPT_AFTER_UNPLAYED_ALL = 'NO EXCEPT AFTER UNPLAYED ALL' +MAP_NO_POWER_AFTER_UNPLAYED_ALL_EXCEPT = 'NO POWER AFTER UNPLAYED ALL EXCEPT' +MAP_NO_SUCH_POWER_TO_REMOVE = 'NO SUCH POWER TO REMOVE: %s' +MAP_RENAMING_UNOWNED_DIR_NOT_ALLOWED = 'RENAMING UNOWNED DIRECTIVE NOT ALLOWED' +MAP_RENAMING_UNDEF_POWER = 'RENAMING UNDEFINED POWER %s' +MAP_RENAMING_POWER_NOT_SUPPORTED = 'THE RENAME POWER OPERATOR -> IS NO LONGER SUPPORTED.' +MAP_POWER_NAME_EMPTY_KEYWORD = 'POWER NAME IS EMPTY KEYWORD: %s' +MAP_POWER_NAME_CAN_BE_CONFUSED = 'POWER NAME CAN BE CONFUSED WITH LOCATION ALIAS OR ORDER TYPE: %s' +MAP_ILLEGAL_POWER_ABBREV = 'ILLEGAL POWER ABBREVIATION' + +GAME_UNKNOWN_POWER = 'UNKNOWN POWER OR PLACENAME: %s' +GAME_UNKNOWN_UNIT_TYPE = 'UNKNOWN UNIT TYPE: %s' +GAME_UNKNOWN_LOCATION = 'UNKNOWN PLACENAME: %s' +GAME_UNKNOWN_COAST = 'UNKNOWN COAST: %s' +GAME_UNKNOWN_ORDER_TYPE = 'UNKNOWN ORDER TYPE: %s' +GAME_FORBIDDEN_RULE = 'RULE %s PREVENTS RULE %s FROM BEING APPLIED.' +GAME_UNRECOGNIZED_ORDER_DATA = 'UNRECOGNIZED DATA IN ORDER: %s' +GAME_AMBIGUOUS_PLACE_NAME = 'AMBIGUOUS PLACENAME: %s' +GAME_BAD_PHASE_NOT_IN_FLOW = 'BAD PHASE (NOT IN FLOW)' +GAME_BAD_BEGIN_PHASE = 'BAD BEGIN PHASE' +GAME_BAD_YEAR_GAME_PHASE = 'BAD YEAR IN GAME PHASE' +GAME_BAD_ADJUSTMENT_ORDER = 'BAD ADJUSTMENT ORDER: %s' +GAME_BAD_RETREAT = 'BAD RETREAT FOR %s: %s' +GAME_ORDER_TO_INVALID_UNIT = 'ORDER TO INVALID UNIT: %s' +GAME_ORDER_INCLUDES_INVALID_UNIT = 'ORDER INCLUDES INVALID UNIT: %s' +GAME_ORDER_INCLUDES_INVALID_DEST = 'ORDER INCLUDES INVALID UNIT DESTINATION %s' +GAME_ORDER_NON_EXISTENT_UNIT = 'ORDER TO NON-EXISTENT UNIT: %s' +GAME_ORDER_TO_FOREIGN_UNIT = 'ORDER TO FOREIGN UNIT: %s' +GAME_UNIT_MAY_ONLY_HOLD = 'UNIT MAY ONLY BE ORDERED TO HOLD: %s' +GAME_CONVOY_IMPROPER_UNIT = 'CONVOY ORDER FOR IMPROPER UNIT: %s %s' +GAME_INVALID_ORDER_NON_EXISTENT_UNIT = 'CANNOT %s NON-EXISTENT UNIT: %s %s' +GAME_INVALID_ORDER_RECIPIENT = 'INVALID %s RECIPIENT: %s %s' +GAME_BAD_ORDER_SYNTAX = 'BAD %s ORDER: %s %s' +GAME_ORDER_RECIPIENT_DOES_NOT_EXIST = '%s RECIPIENT DOES NOT EXIST: %s %s' +GAME_UNIT_CANT_SUPPORT_ITSELF = 'UNIT CANNOT SUPPORT ITSELF: %s %s' +GAME_UNIT_CANT_BE_CONVOYED = 'UNIT CANNOT BE CONVOYED: %s %s' +GAME_BAD_CONVOY_DESTINATION = 'BAD CONVOY DESTINATION: %s %s' +GAME_SUPPORTED_UNIT_CANT_REACH_DESTINATION = 'SUPPORTED UNIT CANNOT REACH DESTINATION: %s %s' +GAME_UNIT_CANT_PROVIDE_SUPPORT_TO_DEST = 'UNIT CANNOT PROVIDE SUPPORT TO DESTINATION: %s %s' +GAME_IMPROPER_CONVOY_ORDER = 'IMPROPER CONVOY ORDER: %s %s' +GAME_IMPROPER_SUPPORT_ORDER = 'IMPROPER SUPPORT ORDER: %s %s' +GAME_IMPOSSIBLE_CONVOY_ORDER = 'IMPOSSIBLE CONVOY ORDER: %s %s' +GAME_BAD_MOVE_ORDER = 'BAD MOVE ORDER: %s %s' +GAME_UNIT_CANT_CONVOY = 'UNIT CANNOT CONVOY: %s %s' +GAME_MOVING_UNIT_CANT_RETURN = 'MOVING UNIT MAY NOT RETURN: %s %s' +GAME_CONVOYING_UNIT_MUST_REACH_COST = 'CONVOYING UNIT MUST REACH COAST: %s %s' +GAME_ARMY_CANT_CONVOY_TO_COAST = 'ARMY CANNOT CONVOY TO SPECIFIC COAST: %s %s' +GAME_CONVOY_UNIT_USED_TWICE = 'CONVOYING UNIT USED TWICE IN SAME CONVOY: %s %s' +GAME_UNIT_CANT_MOVE_INTO_DEST = 'UNIT CANNOT MOVE INTO DESTINATION: %s %s' +GAME_UNIT_CANT_MOVE_VIA_CONVOY_INTO_DEST = 'UNIT CANNOT MOVE VIA CONVOY INTO DESTINATION: %s %s' +GAME_BAD_CONVOY_MOVE_ORDER = 'BAD CONVOY MOVE ORDER: %s %s' +GAME_CONVOY_THROUGH_NON_EXISTENT_UNIT = 'CONVOY THROUGH NON-EXISTENT UNIT: %s %s' +GAME_IMPOSSIBLE_CONVOY = 'IMPOSSIBLE CONVOY: %s %s' +GAME_INVALID_HOLD_ORDER = 'INVALID HOLD ORDER: %s %s' +GAME_UNRECOGNIZED_ORDER_TYPE = 'UNRECOGNIZED ORDER TYPE: %s %s' +GAME_INVALID_RETREAT = 'INVALID RETREAT: %s - %s' +GAME_NO_CONTROL_OVER = 'NO CONTROL OVER %s' +GAME_UNIT_NOT_IN_RETREAT = 'UNIT NOT IN RETREAT: %s' +GAME_TWO_ORDERS_FOR_RETREATING_UNIT = 'TWO ORDERS FOR RETREATING UNIT: %s' +GAME_INVALID_RETREAT_DEST = 'INVALID RETREAT DESTINATION: %s' +GAME_BAD_RETREAT_ORDER = 'BAD RETREAT ORDER: %s' +GAME_DATA_FOR_NON_POWER = 'DATA FOR NON-POWER: %s' +GAME_UNABLE_TO_FIND_RULES = 'UNABLE TO FIND FILE CONTAINING RULES.' +GAME_BUILDS_IN_ALL_ALT_SITES = 'BUILDS IN ALL ALTERNATIVE SITES (%s): %s' +GAME_NO_SUCH_UNIT = 'NO SUCH UNIT: %s' +GAME_MULTIPLE_ORDERS_FOR_UNIT = 'MULTIPLE ORDERS FOR UNIT: %s' +GAME_INVALID_BUILD_SITE = 'INVALID BUILD SITE: %s' +GAME_MULT_BUILDS_IN_SITE = 'MULTIPLE BUILDS IN SITE: %s' +GAME_INVALID_BUILD_ORDER = 'INVALID BUILD ORDER: %s' +GAME_EXCESS_HOME_CENTER_CLAIM = 'EXCESS HOME CENTER CLAIM' + +STD_GAME_BAD_ORDER = 'BAD ORDER: %s' +STD_GAME_UNIT_REORDERED = 'UNIT REORDERED: %s' +STD_GAME_UNORDERABLE_UNIT = 'UNORDERABLE UNIT: %s' diff --git a/diplomacy/utils/exceptions.py b/diplomacy/utils/exceptions.py new file mode 100644 index 0000000..4d564a3 --- /dev/null +++ b/diplomacy/utils/exceptions.py @@ -0,0 +1,178 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Exceptions used in diplomacy network code. """ + +class DiplomacyException(Exception): + """ Diplomacy network code exception. """ + + def __init__(self, message=''): + self.message = (message or self.__doc__).strip() + super(DiplomacyException, self).__init__(self.message) + +class AlreadyScheduledException(DiplomacyException): + """ Cannot add a data already scheduled. """ + +class CommonKeyException(DiplomacyException): + """Common key error.""" + + def __init__(self, key): + super(CommonKeyException, self).__init__('Forbidden common key in two dicts (%s)' % key) + +class KeyException(DiplomacyException): + """ Key error. """ + + def __init__(self, key): + super(KeyException, self).__init__('Key error: %s' % key) + +class LengthException(DiplomacyException): + """ Length error. """ + + def __init__(self, expected_length, given_length): + super(LengthException, self).__init__('Expected length %d, got %d.' % (expected_length, given_length)) + +class NaturalIntegerException(DiplomacyException): + """ Expected a positive integer (int >= 0). """ + + def __init__(self, integer_name=''): + super(NaturalIntegerException, self).__init__( + ('Integer error: %s.%s' % (integer_name, self.__doc__)) if integer_name else '') + +class NaturalIntegerNotNullException(NaturalIntegerException): + """ Expected a strictly positive integer (int > 0). """ + +class RandomPowerException(DiplomacyException): + """ No enough playable powers to select random powers. """ + + def __init__(self, nb_powers, nb_available_powers): + super(RandomPowerException, self).__init__('Cannot randomly select %s power(s) in %s available power(s).' + % (nb_powers, nb_available_powers)) + +class TypeException(DiplomacyException): + """ Type error. """ + + def __init__(self, expected_type, given_type): + super(TypeException, self).__init__('Expected type %s, got type %s' % (expected_type, given_type)) + +class ValueException(DiplomacyException): + """ Value error. """ + + def __init__(self, expected_values, given_value): + super(ValueException, self).__init__('Forbidden value %s, expected: %s' + % (given_value, ', '.join(str(v) for v in expected_values))) + +class NotificationException(DiplomacyException): + """ Unknown notification. """ + +class ResponseException(DiplomacyException): + """ Unknown response. """ + +class RequestException(ResponseException): + """ Unknown request. """ + +class AdminTokenException(ResponseException): + """ Invalid token for admin operations. """ + +class GameCanceledException(ResponseException): + """ Game was cancelled. """ + +class GameCreationException(ResponseException): + """ Cannot create more games on that server. """ + +class GameFinishedException(ResponseException): + """ This game is finished. """ + +class GameIdException(ResponseException): + """ Invalid game ID. """ + +class GameJoinRoleException(ResponseException): + """ A token can have only one role inside a game: player, observer or omniscient. """ + +class GameMasterTokenException(ResponseException): + """ Invalid token for master operations. """ + +class GameNotPlayingException(ResponseException): + """ Game not playing. """ + +class GameObserverException(ResponseException): + """ Disallowed observation for non-master users. """ + +class GamePhaseException(ResponseException): + """ Data does not match current game phase. """ + + def __init__(self, expected=None, given=None): + message = self.__doc__.strip() + # This is to prevent an unexpected Pycharm warning about message type. + if isinstance(message, bytes): + message = message.decode() + if expected is not None: + message += ' Expected: %s' % expected + if given is not None: + message += ' Given: %s' % given + super(GamePhaseException, self).__init__(message) + +class GamePlayerException(ResponseException): + """ Invalid player. """ + +class GameRegistrationPasswordException(ResponseException): + """ Invalid game registration password. """ + +class GameSolitaireException(ResponseException): + """ A solitaire game does not accepts players. """ + +class GameTokenException(ResponseException): + """ Invalid token for this game. """ + +class MapIdException(ResponseException): + """ Invalid map ID. """ + +class MapPowerException(ResponseException): + """ Invalid map power. """ + + def __init__(self, power_name): + super(MapPowerException, self).__init__('Invalid map power %s' % power_name) + +class ServerDataDirException(ResponseException): + """ No data directory available in server folder. """ + +class FolderException(ResponseException): + """ Given folder not available in server. """ + def __init__(self, folder_path): + super(FolderException, self).__init__('Given folder not available in server: %s' % folder_path) + +class ServerGameDirException(ResponseException): + """ No games directory available in server/data folder. """ + +class ServerRegistrationException(ResponseException): + """ Registration currently not allowed on this server. """ + +class TokenException(ResponseException): + """ Invalid token. """ + +class UserException(ResponseException): + """ Invalid user. """ + +class PasswordException(ResponseException): + """ Password must not be empty. """ + +class VoteCreationException(ResponseException): + """ Only either a player or a game master for a game with at least 1 player can create a vote. """ + +class ServerDirException(ResponseException): + """ Error with working folder. """ + + def __init__(self, server_dir): + super(ServerDirException, self).__init__("No server directory available at path %s" % server_dir) 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 diff --git a/diplomacy/utils/game_phase_data.py b/diplomacy/utils/game_phase_data.py new file mode 100644 index 0000000..c45eb63 --- /dev/null +++ b/diplomacy/utils/game_phase_data.py @@ -0,0 +1,47 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Utility class to save all data related to one game phase (phase name, state, messages and orders). """ +from diplomacy.engine.message import Message +from diplomacy.utils import strings, parsing +from diplomacy.utils.jsonable import Jsonable +from diplomacy.utils.sorted_dict import SortedDict + +MESSAGES_TYPE = parsing.IndexedSequenceType( + parsing.DictType(int, parsing.JsonableClassType(Message), SortedDict.builder(int, Message)), 'time_sent') + +class GamePhaseData(Jsonable): + """ Small class to represent data for a game phase: + phase name, state, orders, orders results and messages for this phase. + """ + __slots__ = ['name', 'state', 'orders', 'results', 'messages'] + + model = { + strings.NAME: str, + strings.STATE: dict, + strings.ORDERS: parsing.DictType(str, parsing.OptionalValueType(parsing.SequenceType(str))), + strings.RESULTS: parsing.DictType(str, parsing.SequenceType(str)), + strings.MESSAGES: MESSAGES_TYPE, + } + + def __init__(self, name, state, orders, results, messages): + """ Constructor. """ + self.name = '' + self.state = {} + self.orders = {} + self.results = {} + self.messages = {} + super(GamePhaseData, self).__init__(name=name, state=state, orders=orders, results=results, messages=messages) diff --git a/diplomacy/utils/jsonable.py b/diplomacy/utils/jsonable.py new file mode 100644 index 0000000..5e558ee --- /dev/null +++ b/diplomacy/utils/jsonable.py @@ -0,0 +1,141 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Abstract Jsonable class with automatic attributes checking and conversion to/from JSON dict. + To write a Jsonable sub-class: + - Define a model with expected attribute names and types. Use module `parsing` to describe expected types. + - Override initializer __init__(**kwargs): + - **first**: initialize each attribute defined in model with value None. + - **then** : call parent __init__() method. Attributes will be checked and filled by + Jsonable's __init__() method. + - If needed, add further initialization code after call to parent __init__() method. At this point, + attributes were correctly set based on defined model, and you can now work with them. + + Example: + ``` + class MyClass(Jsonable): + model = { + 'my_attribute': parsing.Sequence(int), + } + def __init__(**kwargs): + self.my_attribute = None + super(MyClass, self).__init__(**kwargs) + # my_attribute is now initialized based on model. You can then do any further initialization if needed. + ``` +""" +import logging +import ujson as json + +from diplomacy.utils import exceptions, parsing + +LOGGER = logging.getLogger(__name__) + +class Jsonable(): + """ Abstract class to ease conversion from/to JSON dict. """ + __slots__ = [] + __cached__models__ = {} + model = {} + + def __init__(self, **kwargs): + """ Validates given arguments, update them if necessary (e.g. to add default values), + and fill instance attributes with updated argument. + If a derived class adds new attributes, it must override __init__() method and + initialize new attributes (e.g. `self.attribute = None`) + **BEFORE** calling parent __init__() method. + + :param kwargs: arguments to build class. Must match keys and values types defined in model. + """ + model = self.get_model() + + # Adding default value + updated_kwargs = {model_key: None for model_key in model} + updated_kwargs.update(kwargs) + + # Validating and updating + try: + parsing.validate_data(updated_kwargs, model) + except exceptions.TypeException as exception: + LOGGER.error('Error occurred while building class %s', self.__class__) + raise exception + updated_kwargs = parsing.update_data(updated_kwargs, model) + + # Building. + for model_key in model: + setattr(self, model_key, updated_kwargs[model_key]) + + def json(self): + """ Convert this object to a JSON string ready to be sent/saved. + :return: string + """ + return json.dumps(self.to_dict()) + + def to_dict(self): + """ Convert this object to a python dictionary ready for any JSON work. + :return: dict + """ + model = self.get_model() + return {key: parsing.to_json(getattr(self, key), key_type) for key, key_type in model.items()} + + @classmethod + def update_json_dict(cls, json_dict): + """ Update a JSON dictionary before being parsed with class model. + JSON dictionary is passed by class method from_dict() (see below), and is guaranteed to contain + at least all expected model keys. Some keys may be associated to None if initial JSON dictionary + did not provide values for them. + :param json_dict: a JSON dictionary to be parsed. + :type json_dict: dict + """ + pass + + @classmethod + def from_dict(cls, json_dict): + """ Convert a JSON dictionary to an instance of this class. + :param json_dict: a JSON dictionary to parse. Dictionary with basic types (int, bool, dict, str, None, etc.) + :return: an instance from this class or from a derived one from which it's called. + :rtype: cls + """ + model = cls.get_model() + + # json_dict must be a a dictionary + if not isinstance(json_dict, dict): + raise exceptions.TypeException(dict, type(json_dict)) + + # By default, we set None for all expected keys + default_json_dict = {key: None for key in model} + default_json_dict.update(json_dict) + cls.update_json_dict(json_dict) + + # Building this object + # NB: We don't care about extra keys in provided dict, we just focus on expected keys, nothing more. + kwargs = {key: parsing.to_type(default_json_dict[key], key_type) for key, key_type in model.items()} + return cls(**kwargs) + + @classmethod + def build_model(cls): + """ Return model associated to current class. You can either define model class field + or override this function. + """ + return cls.model + + @classmethod + def get_model(cls): + """ Return model associated to current class, and cache it for future uses, to avoid + multiple rendering of model for each class derived from Jsonable. Private method. + :return: dict: model associated to current class. + """ + if cls not in cls.__cached__models__: + cls.__cached__models__[cls] = cls.build_model() + return cls.__cached__models__[cls] diff --git a/diplomacy/utils/keywords.py b/diplomacy/utils/keywords.py new file mode 100644 index 0000000..cdd9362 --- /dev/null +++ b/diplomacy/utils/keywords.py @@ -0,0 +1,39 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Aliases and keywords + - Contains aliases and keywords + - Keywords are always single words + - Aliases are only converted in a second pass, so if they contain a keyword, you should replace + the keyword with its abbreviation. +""" + +KEYWORDS = {'>': '', '-': '-', 'ARMY': 'A', 'FLEET': 'F', 'WING': 'W', 'THE': '', 'NC': '/NC', 'SC': '/SC', + 'EC': '/EC', 'WC': '/WC', 'MOVE': '', 'MOVES': '', 'MOVING': '', 'ATTACK': '', 'ATTACKS': '', + 'ATTACKING': '', 'RETREAT': 'R', 'RETREATS': 'R', 'RETREATING': 'R', 'SUPPORT': 'S', 'SUPPORTS': 'S', + 'SUPPORTING': 'S', 'CONVOY': 'C', 'CONVOYS': 'C', 'CONVOYING': 'C', 'HOLD': 'H', 'HOLDS': 'H', + 'HOLDING': 'H', 'BUILD': 'B', 'BUILDS': 'B', 'BUILDING': 'B', 'DISBAND': 'D', 'DISBANDS': 'D', + 'DISBANDING': 'D', 'DESTROY': 'D', 'DESTROYS': 'D', 'DESTROYING': 'D', 'REMOVE': 'D', 'REMOVES': 'D', + 'REMOVING': 'D', 'WAIVE': 'V', 'WAIVES': 'V', 'WAIVING': 'V', 'WAIVED': 'V', 'KEEP': 'K', 'KEEPS': 'K', + 'KEEPING': 'K', 'PROXY': 'P', 'PROXIES': 'P', 'PROXYING': 'P', 'IS': '', 'WILL': '', 'IN': '', 'AT': '', + 'ON': '', 'TO': '', 'OF': '\\', 'FROM': '\\', 'WITH': '?', 'TSR': '=', 'VIA': 'VIA', 'THROUGH': '~', + 'OVER': '~', 'BY': '~', 'OR': '|', 'BOUNCE': '|', 'CUT': '|', 'VOID': '?', 'DISLODGED': '~', + 'DESTROYED': '*'} + +ALIASES = {'NORTH COAST \\': '/NC \\', 'SOUTH COAST \\': '/SC \\', 'EAST COAST \\': '/EC \\', + 'WEST COAST \\': '/WC \\', 'AN A': 'A', 'A F': 'F', 'A W': 'W', 'NO C': '?', '~ C': '^', + '~ =': '=', '? =': '=', '~ LAND': '_', '~ WATER': '_', '~ SEA': '_', 'VIA C': 'VIA', + 'TRANS SIBERIAN RAILROAD': '=', 'V B': 'B V'} diff --git a/diplomacy/utils/network_data.py b/diplomacy/utils/network_data.py new file mode 100644 index 0000000..48b285a --- /dev/null +++ b/diplomacy/utils/network_data.py @@ -0,0 +1,79 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Abstract Jsonable class to create data intended to be exchanged on network. + Used for requests, responses and notifications. + To write a sub-class, you must first write a base class for data category (e.g. notifications): + + - Define header model for network data. + + - Define ID field for data category (e.g. "notification_id"). This will be used to create unique + identifier for every data exchanged on network. + + - Then every sub-class from base class must define parameters (params) model. Params and header + must not share any field. +""" +import uuid + +from diplomacy.utils import strings, exceptions +from diplomacy.utils.common import assert_no_common_keys, camel_case_to_snake_case +from diplomacy.utils.jsonable import Jsonable + +class NetworkData(Jsonable): + """ Abstract class for network-exchanged data. """ + __slots__ = ['name'] + # NB: header must have a `name` field and a field named `id_field`. + header = {} + params = {} + id_field = None + + def __init__(self, **kwargs): + self.name = None # type: str + + # Setting default values + kwargs[strings.NAME] = kwargs.get(strings.NAME, None) or self.get_class_name() + kwargs[self.id_field] = kwargs.get(self.id_field, None) or str(uuid.uuid4()) + if kwargs[strings.NAME] != self.get_class_name(): + raise exceptions.DiplomacyException('Expected request name %s, got %s' % + (self.get_class_name(), kwargs[strings.NAME])) + + # Building + super(NetworkData, self).__init__(**kwargs) + + @classmethod + def get_class_name(cls): + """ Returns the class name in snake_case. """ + return camel_case_to_snake_case(cls.__name__) + + @classmethod + def validate_params(cls): + """ Called when getting model to validate parameters. Called once per class. """ + pass + + @classmethod + def build_model(cls): + """ Return model associated to current class. You can either define model class field + or override this function. + """ + # Validating model parameters (header and params must have different keys) + assert_no_common_keys(cls.header, cls.params) + cls.validate_params() + + # Building model. + model = cls.header.copy() + model.update(cls.params.copy()) + model[strings.NAME] = (cls.get_class_name(),) + return model diff --git a/diplomacy/utils/parsing.py b/diplomacy/utils/parsing.py new file mode 100644 index 0000000..802fab3 --- /dev/null +++ b/diplomacy/utils/parsing.py @@ -0,0 +1,505 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Provide classes and methods to parse python objects. + + Useful for type checking and conversions from/to JSON dictionaries. + + This module use 2 definitions to distinguish values from/to JSON: item values and attribute values. + + Item value is a value retrieved from a JSON dictionary. It's generally a basic Python type + (e.g. bool, int, str, float). + + Attribute value is a value used in Python code and expected by type checking. It may be + a basic Python type, or a class instance. Note that not all classes are allowed (see + other type checkers below). + +""" +import inspect +import logging +from abc import ABCMeta, abstractmethod +from copy import copy + +from diplomacy.utils import exceptions +from diplomacy.utils.common import assert_no_common_keys, is_dictionary, is_sequence + +LOGGER = logging.getLogger(__name__) + +# ----------------------------------------------------- +# ------------ Functions --------------- +# ----------------------------------------------------- + +def update_model(model, additional_keys, allow_duplicate_keys=True): + """ Return a copy of model updated with additional keys. + :param model: (Dictionary). Model to extend + :param additional_keys: (Dictionary). Definition of the additional keys to use to update the model. + :param allow_duplicate_keys: Boolean. If True, the model key will be updated if present in additional keys. + Otherwise, an error is thrown if additional_key contains a model key. + :return: The updated model with the additional keys. + """ + assert isinstance(model, dict) + assert isinstance(additional_keys, dict) + if not allow_duplicate_keys: + assert_no_common_keys(model, additional_keys) + model_copy = model.copy() + model_copy.update(additional_keys) + return model_copy + +def extend_model(model, additional_keys): + """ Return a copy of model updated with additional model keys. Model and additional keys must no share any key. + :param model: (Dictionary). Model to update + :param additional_keys: (Dictionary). Definition of the additional keys to add to model. + :return: The updated model with the additional keys. + """ + return update_model(model, additional_keys, allow_duplicate_keys=False) + +def get_type(desired_type): + """ Return a ParserType sub-class that matches given type. + :param desired_type: basic type or ParserType sub-class. + :return: ParserType sub-class instance. + """ + # Already a ParserType, we return the object directly. + if isinstance(desired_type, ParserType): + return desired_type + + # Sequence of primitive. + # Detecting if we have a sequence of primitive classes or instances (values). + if isinstance(desired_type, (list, tuple, set)) and desired_type: + if inspect.isclass(next(iter(desired_type))): + return SequenceOfPrimitivesType(desired_type) + return EnumerationType(desired_type) + + # By default, we return a Type(expected_type). + # If expected_type is not a basic type, an exception will be raised + # (see class Type above). + return PrimitiveType(desired_type) + +def to_type(json_value, parser_type): + """ Convert a JSON value (python built-in type) to the type + described by parser_type. + :param json_value: JSON value to convert. + :param parser_type: either an instance of a ParserType, or a type convertible + to a ParserType (see function get_type() above). + :return: JSON value converted to expected type. + """ + return get_type(parser_type).to_type(json_value) + +def to_json(raw_value, parser_type): + """ Convert a value from the type described by parser_type to a JSON value. + :param raw_value: The raw value to convert to JSON. + :param parser_type: Either an instance of a ParserType, or a type convertible to a ParserType. + :return: The value converted to an equivalent JSON value. + """ + return get_type(parser_type).to_json(raw_value) + +def validate_data(data, model): + """ Validates that the data complies with the model + :param data: (Dictionary). A dict of values to validate against the model. + :param model: (Dictionary). The model to use for validation. + """ + assert isinstance(data, dict) + assert isinstance(model, dict) + + # Make sure all fields in data are of the correct type. + # Also make sure all expected fields not present in data have default values (e.g. None). + # NB: We don't care about extra keys in provided data. We only focus on expected keys. + for model_key, model_type in model.items(): + try: + get_type(model_type).validate(data.get(model_key, None)) + except exceptions.TypeException as exception: + LOGGER.error('Error occurred while checking key %s', model_key) + raise exception + +def update_data(data, model): + """ Modifies the data object to add default values if needed + :param data: (Dictionary). A dict of values to update. + :param model: (Dictionary). The model to use. + """ + # Updating the data types + for model_key, model_type in model.items(): + data_value = data.get(model_key, None) + # update() will return either same value or updated value. + data[model_key] = get_type(model_type).update(data_value) + return data + +# ----------------------------------------------------- +# ------------ Classes --------------- +# ----------------------------------------------------- + +class ParserType(metaclass=ABCMeta): + """ Abstract base class to check a specific type. """ + __slots__ = [] + # We include dict into primitive types to allow parser to accept raw untyped dict (e.g. engine game state). + primitives = (int, float, bool, str, dict) + + @abstractmethod + def validate(self, element): + """ Makes sure the element is a valid element for this parser type + :param element: The element to validate. + :return: None, but raises Error if needed. + """ + raise NotImplementedError() + + def update(self, element): + """ Returns the correct value to use in the data object. + :param element: The element the model wants to store in the data object of this parser type. + :return: The updated element to store in the data object. + The updated element might be a different value (e.g. if a default value is present) + """ + # pylint: disable=no-self-use + return element + + def to_type(self, json_value): + """ Converts a json_value to this parser type. + :param json_value: The JSON value to convert. + :return: The converted JSON value. + """ + # pylint: disable=no-self-use + return json_value + + def to_json(self, raw_value): + """ Converts a raw value (of this type) to JSON. + :param raw_value: The raw value (of this type) to convert. + :return: The resulting JSON value. + """ + # pylint: disable=no-self-use + return raw_value + +class ConverterType(ParserType): + """ Type checker that allows to use another parser type with a converter function. + Converter function will be used to convert any raw value to a value expected + by given parser type before validations and updates. + """ + def __init__(self, element_type, converter_function, json_converter_function=None): + """ Initialize a converter type. + :param element_type: expected type + :param converter_function: function to be used to check and convert values to expected type. + converter_function(value) -> value_compatible_with_expected_type + :param json_converter_function: function to be used to convert a JSON value + to an expected JSON value for element_type. If not provided, converter_function will be used. + json_converter_function(json_value) -> new JSON value valid for element_type.to_type(new_json_value) + """ + element_type = get_type(element_type) + assert not isinstance(element_type, ConverterType) + assert callable(converter_function) + self.element_type = element_type + self.converter_function = converter_function + self.json_converter_function = json_converter_function or converter_function + + def validate(self, element): + self.element_type.validate(self.converter_function(element)) + + def update(self, element): + return self.element_type.update(self.converter_function(element)) + + def to_type(self, json_value): + return self.element_type.to_type(self.json_converter_function(json_value)) + + def to_json(self, raw_value): + return self.element_type.to_json(raw_value) + +class DefaultValueType(ParserType): + """ Type checker that allows a default value. """ + __slots__ = ('element_type', 'default_json_value') + + def __init__(self, element_type, default_json_value): + """ Initialize a default type checker with expected element type and a default value (if None is present). + :param element_type: The expected type for elements (except if None is provided). + :param default_json_value: The default value to set if element=None. Must be a JSON value + convertible to element_type, so that new default value is generated from this JSON value + each time it's needed. + """ + element_type = get_type(element_type) + assert not isinstance(element_type, (DefaultValueType, OptionalValueType)) + self.element_type = element_type + self.default_json_value = default_json_value + # If default JSON value is provided, make sure it's a valid value. + if default_json_value is not None: + self.validate(self.to_type(default_json_value)) + + def __str__(self): + """ String representation """ + return '%s (default %s)' % (self.element_type, self.default_json_value) + + def validate(self, element): + if element is not None: + self.element_type.validate(element) + + def update(self, element): + if element is not None: + return self.element_type.update(element) + return None if self.default_json_value is None else self.element_type.to_type(self.default_json_value) + + def to_type(self, json_value): + json_value = self.default_json_value if json_value is None else json_value + return None if json_value is None else self.element_type.to_type(json_value) + + def to_json(self, raw_value): + return copy(self.default_json_value) if raw_value is None else self.element_type.to_json(raw_value) + +class OptionalValueType(DefaultValueType): + """ Type checker that allows None as default value. """ + __slots__ = [] + + def __init__(self, element_type): + """ Initialized a optional type checker with expected element type. + :param element_type: The expected type for elements. + """ + super(OptionalValueType, self).__init__(element_type, None) + +class SequenceType(ParserType): + """ Type checker for sequence-like objects. """ + __slots__ = ['element_type', 'sequence_builder'] + + def __init__(self, element_type, sequence_builder=None): + """ Initialize a sequence type checker with value type and optional sequence builder. + :param element_type: Expected type for sequence elements. + :param sequence_builder: (Optional). A callable used to build the sequence type. + Expected args: Iterable + """ + self.element_type = get_type(element_type) + self.sequence_builder = sequence_builder if sequence_builder is not None else lambda seq: seq + + def __str__(self): + """ String representation """ + return '[%s]' % self.element_type + + def validate(self, element): + if not is_sequence(element): + raise exceptions.TypeException('sequence', type(element)) + for seq_element in element: + self.element_type.validate(seq_element) + + def update(self, element): + # Converting each element in the list, then using the seq builder if available + sequence = [self.element_type.update(seq_element) for seq_element in element] + return self.sequence_builder(sequence) + + def to_type(self, json_value): + sequence = [self.element_type.to_type(seq_element) for seq_element in json_value] + return self.sequence_builder(sequence) + + def to_json(self, raw_value): + return [self.element_type.to_json(seq_element) for seq_element in raw_value] + +class JsonableClassType(ParserType): + """ Type checker for Jsonable classes. """ + __slots__ = ['element_type'] + + def __init__(self, jsonable_element_type): + """ Initialize a sub-class of Jsonable. + :param jsonable_element_type: Expected type (should be a subclass of Jsonable). + """ + # We import Jsonable here to prevent recursive import with module jsonable. + from diplomacy.utils.jsonable import Jsonable + assert issubclass(jsonable_element_type, Jsonable) + self.element_type = jsonable_element_type + + def __str__(self): + """ String representation """ + return self.element_type.__name__ + + def validate(self, element): + if not isinstance(element, self.element_type): + raise exceptions.TypeException(self.element_type, type(element)) + + def to_type(self, json_value): + return self.element_type.from_dict(json_value) + + def to_json(self, raw_value): + return raw_value.to_dict() + +class StringableType(ParserType): + """ Type checker for a class that can be converted to a string with str(obj) + and converted from a string with cls.from_string(str_val) or cls(str_val). + + In practice, this parser will just save object as string with str(obj), + and load object from string using cls(str_val) or cls.from_string(str_val). + So, object may have any type as long as: + str(obj) == str( object loaded from str(obj) ) + + Expected type: a class with compatible str(cls(string_repr)) or str(cls.from_string(string_repr)). + """ + __slots__ = ['element_type', 'use_from_string'] + + def __init__(self, element_type): + """ Initialize a parser type with a type convertible from/to string. + :param element_type: Expected type. Needs to be convertible to/from String. + """ + if hasattr(element_type, 'from_string'): + assert callable(element_type.from_string) + self.use_from_string = True + else: + self.use_from_string = False + self.element_type = element_type + + def __str__(self): + """ String representation """ + return self.element_type.__name__ + + def validate(self, element): + if not isinstance(element, self.element_type): + try: + # Check if given element can be handled by element type. + element_to_str = self.to_json(element) + element_from_str = self.to_type(element_to_str) + element_from_str_to_str = self.to_json(element_from_str) + assert element_to_str == element_from_str_to_str + except Exception: + # Given element can't be handled, raise a type exception. + raise exceptions.TypeException(self.element_type, type(element)) + + def to_type(self, json_value): + if self.use_from_string: + return self.element_type.from_string(json_value) + return self.element_type(json_value) + + def to_json(self, raw_value): + return str(raw_value) + +class DictType(ParserType): + """ Type checking for dictionary-like objects. """ + __slots__ = ['key_type', 'val_type', 'dict_builder'] + + def __init__(self, key_type, val_type, dict_builder=None): + """ Initialize a dict parser type with expected key type, val type, and optional dict builder. + :param key_type: The expected key type. Must be string or a stringable class. + :param val_type: The expected value type. + :param dict_builder: Callable to build attribute values. + """ + # key type muse be convertible from/to string. + self.key_type = key_type if isinstance(key_type, StringableType) else StringableType(key_type) + self.val_type = get_type(val_type) + self.dict_builder = dict_builder if dict_builder is not None else lambda dictionary: dictionary + + def __str__(self): + """ String representation """ + return '{%s => %s}' % (self.key_type, self.val_type) + + def validate(self, element): + if not is_dictionary(element): + raise exceptions.TypeException('dictionary', type(element)) + for key, value in element.items(): + self.key_type.validate(key) + self.val_type.validate(value) + + def update(self, element): + return_dict = {self.key_type.update(key): self.val_type.update(value) for key, value in element.items()} + return self.dict_builder(return_dict) + + def to_type(self, json_value): + json_dict = {self.key_type.to_type(key): self.val_type.to_type(value) for key, value in json_value.items()} + return self.dict_builder(json_dict) + + def to_json(self, raw_value): + return {self.key_type.to_json(key): self.val_type.to_json(value) for key, value in raw_value.items()} + +class IndexedSequenceType(ParserType): + """ Parser for objects stored as dictionaries in memory and saved as lists in JSON. """ + __slots__ = ['dict_type', 'sequence_type', 'key_name'] + + def __init__(self, dict_type, key_name): + """ Initializer: + :param dict_type: dictionary parser type to be used to manage object in memory. + :param key_name: name of attribute to take in sequence elements to convert sequence to a dictionary. + dct = {getattr(element, key_name): element for element in sequence} + sequence = list(dct.values()) + """ + assert isinstance(dict_type, DictType) + self.dict_type = dict_type + self.sequence_type = SequenceType(self.dict_type.val_type) + self.key_name = str(key_name) + + def __str__(self): + return '{%s.%s}' % (self.dict_type.val_type, self.key_name) + + def validate(self, element): + self.dict_type.validate(element) + + def update(self, element): + return self.dict_type.update(element) + + def to_json(self, raw_value): + """ Dict is saved as a sequence. """ + return self.sequence_type.to_json(raw_value.values()) + + def to_type(self, json_value): + """ JSON is parsed as a sequence and converted to a dict. """ + loaded_sequence = self.sequence_type.to_type(json_value) + return self.dict_type.update({getattr(element, self.key_name): element for element in loaded_sequence}) + +class EnumerationType(ParserType): + """ Type checker for a set of allowed basic values. """ + __slots__ = ['enum_values'] + + def __init__(self, enum_values): + """ Initialize sequence of values type with a sequence of allowed (primitive) values. + :param enum_values: Sequence of allowed values. + """ + enum_values = set(enum_values) + assert enum_values and all(isinstance(value, self.primitives) for value in enum_values) + self.enum_values = enum_values + + def __str__(self): + """ String representation """ + return 'in (%s)' % (', '.join(str(e) for e in sorted(self.enum_values))) + + def validate(self, element): + if not any(type(element) is type(value) and element == value for value in self.enum_values): + raise exceptions.ValueException(self.enum_values, element) + + def to_type(self, json_value): + """ For enumerations, we will validate JSON value before parsing it. """ + self.validate(json_value) + return json_value + +class SequenceOfPrimitivesType(ParserType): + """ Type checker for a set of allowed basic types. """ + __slots__ = ['seq_of_primitives'] + + def __init__(self, seq_of_primitives): + """ Initialize sequence of primitives type with a sequence of allowed primitives. + :param seq_of_primitives: Sequence of primitives. + """ + assert seq_of_primitives and all(primitive in self.primitives for primitive in seq_of_primitives) + self.seq_of_primitives = seq_of_primitives if isinstance(seq_of_primitives, tuple) else tuple(seq_of_primitives) + + def __str__(self): + """ String representation """ + return 'type in: %s' % (', '.join(t.__name__ for t in self.seq_of_primitives)) + + def validate(self, element): + if not isinstance(element, self.seq_of_primitives): + raise exceptions.TypeException(self.seq_of_primitives, type(element)) + +class PrimitiveType(ParserType): + """ Type checker for a primitive type. """ + __slots__ = ['element_type'] + + def __init__(self, element_type): + """ Initialize a primitive type. + :param element_type: Primitive type. + """ + assert element_type in self.primitives, 'Expected a primitive type, got %s.' % element_type + self.element_type = element_type + + def __str__(self): + """ String representation """ + return self.element_type.__name__ + + def validate(self, element): + if not isinstance(element, self.element_type): + raise exceptions.TypeException(self.element_type, type(element)) diff --git a/diplomacy/utils/priority_dict.py b/diplomacy/utils/priority_dict.py new file mode 100644 index 0000000..99a75c9 --- /dev/null +++ b/diplomacy/utils/priority_dict.py @@ -0,0 +1,102 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Priority Dict implementation """ +import heapq + +# ------------------------------------------------ +# Adapted from (2018/03/14s): https://docs.python.org/3.6/library/heapq.html#priority-queue-implementation-notes +# Unlicensed +class PriorityDict(dict): + """ Priority Dictionary Implementation """ + + def __init__(self, **kwargs): + """ Initialize the priority queue. + :param kwargs: (optional) initial values for priority queue. + """ + self.__heap = [] # Heap for entries. An entry is a triple (priority value, key, valid entry flag (boolean)). + # Dict itself maps key to entries. We override some dict methods (see __getitem__() below) + # to always return priority value instead of entry as dict value. + dict.__init__(self) + for key, value in kwargs.items(): + self[key] = value + + def __setitem__(self, key, val): + """ Sets a key with his associated priority + :param key: The key to set in the dictionary + :param val: The priority to associate with the key + :return: None + """ + if key in self: + del self[key] + # Create entry with val, key and a boolean indicating that entry is valid (True). + entry = [val, key, True] + dict.__setitem__(self, key, entry) + heapq.heappush(self.__heap, entry) + + def __delitem__(self, key): + """ Removes key from dict and marks associated heap entry as invalid (False). Raises KeyError if not found. """ + entry = self.pop(key) + entry[-1] = False + + def __getitem__(self, key): + """ Returns priority value associated to key. Raises KeyError if key not found. """ + return dict.__getitem__(self, key)[0] + + def __iter__(self): + """ Iterator over all keys based on their priority. """ + + def iterfn(): + """ Iterator """ + copy_of_self = self.copy() + while copy_of_self: + _, key = copy_of_self.smallest() + del copy_of_self[key] + yield key + + return iterfn() + + def smallest(self): + """ Finds the smallest item in the priority dict + :return: A tuple of (priority, key) for the item with the smallest priority + """ + while self.__heap and not self.__heap[0][-1]: + heapq.heappop(self.__heap) + return self.__heap[0][:2] if self.__heap else None + + def setdefault(self, key, d=None): + """ Sets a default for a given key """ + if key not in self: + self[key] = d + return self[key] + + def copy(self): + """ Return a copy of this priority dict. + :rtype: PriorityDict + """ + return PriorityDict(**self) + + def keys(self): + """ Make sure keys() iterates on keys based on their priority. """ + return self.__iter__() + + def values(self): + """ Makes sure values() iterates on priority values (instead of heap entries) from smallest to highest. """ + return (self[k] for k in self) + + def items(self): + """ Makes sure items() values are priority values instead of heap entries. """ + return ((key, self[key]) for key in self) diff --git a/diplomacy/utils/scheduler_event.py b/diplomacy/utils/scheduler_event.py new file mode 100644 index 0000000..1d097d8 --- /dev/null +++ b/diplomacy/utils/scheduler_event.py @@ -0,0 +1,42 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Scheduler event describing scheduler state for a specific data. """ + +from diplomacy.utils.jsonable import Jsonable + +class SchedulerEvent(Jsonable): + """ Scheduler event class. Properties: + - time_unit: unit time (in seconds) used by scheduler (time between 2 tasks checkings). + Currently 1 second in server scheduler. + - time_added: scheduler time (nb. time units) when data was added to scheduler. + - delay: scheduler time (nb. time units) to wait before processing time. + - current_time: current scheduler time (nb. time units). + """ + __slots__ = ['time_unit', 'time_added', 'delay', 'current_time'] + model = { + 'time_unit': int, + 'time_added': int, + 'delay': int, + 'current_time': int + } + + def __init__(self, **kwargs): + self.time_unit = 0 + self.time_added = 0 + self.delay = 0 + self.current_time = 0 + super(SchedulerEvent, self).__init__(**kwargs) diff --git a/diplomacy/utils/sorted_dict.py b/diplomacy/utils/sorted_dict.py new file mode 100644 index 0000000..459c652 --- /dev/null +++ b/diplomacy/utils/sorted_dict.py @@ -0,0 +1,259 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Helper class to provide a dict with sorted keys. """ +from diplomacy.utils.common import is_dictionary +from diplomacy.utils.sorted_set import SortedSet + +class SortedDict(): + """ Dict with sorted keys. """ + __slots__ = ['__val_type', '__keys', '__couples'] + + def __init__(self, key_type, val_type, kwargs=None): + """ Initialize a typed SortedDict. + :param key_type: expected type for keys. + :param val_type: expected type for values. + :param kwargs: (optional) dictionary-like object: initial values for sorted dict. + """ + self.__val_type = val_type + self.__keys = SortedSet(key_type) + self.__couples = {} + if kwargs is not None: + assert is_dictionary(kwargs) + for key, value in kwargs.items(): + self.put(key, value) + + @staticmethod + def builder(key_type, val_type): + """ Return a function to build sorted dicts from a dictionary-like object. + Returned function expects a dictionary parameter (an object with method items()). + builder_fn = SortedDict.builder(str, int) + my_sorted_dict = builder_fn({'a': 1, 'b': 2}) + + :param key_type: expected type for keys. + :param val_type: expected type for values. + :return: callable + """ + return lambda dictionary: SortedDict(key_type, val_type, dictionary) + + @property + def key_type(self): + """ Get key type. """ + return self.__keys.element_type + + @property + def val_type(self): + """ Get value type. """ + return self.__val_type + + def __str__(self): + return 'SortedDict{%s}' % ', '.join('%s:%s' % (k, self.__couples[k]) for k in self.__keys) + + def __bool__(self): + return bool(self.__keys) + + def __len__(self): + return len(self.__keys) + + def __eq__(self, other): + """ Return True if self and other are equal. + Note that self and other must also have same key and value types. + """ + assert isinstance(other, SortedDict) + return (self.key_type is other.key_type + and self.val_type is other.val_type + and len(self) == len(other) + and all(key in other and self[key] == other[key] for key in self.__keys)) + + def __getitem__(self, key): + return self.__couples[key] + + def __setitem__(self, key, value): + self.put(key, value) + + def __delitem__(self, key): + self.remove(key) + + def __iter__(self): + return self.__keys.__iter__() + + def __contains__(self, key): + return key in self.__couples + + def get(self, key, default=None): + """ Return value associated with key, or default value if key not found. """ + return self.__couples.get(key, default) + + def put(self, key, value): + """ Add a key with a value to the dict. """ + if not isinstance(value, self.__val_type): + raise TypeError('Expected value type %s, got %s' % (self.__val_type, type(value))) + if key not in self.__keys: + self.__keys.add(key) + self.__couples[key] = value + + def remove(self, key): + """ Pop (remove and return) value associated with given key, or None if key not found. """ + if key in self.__couples: + self.__keys.remove(key) + return self.__couples.pop(key, None) + + def first_key(self): + """ Get the lowest key from the dict. """ + return self.__keys[0] + + def first_value(self): + """ Get the value associated to lowest key in the dict. """ + return self.__couples[self.__keys[0]] + + def last_key(self): + """ Get the highest key from the dict. """ + return self.__keys[-1] + + def last_value(self): + """ Get the value associated to highest key in the dict. """ + return self.__couples[self.__keys[-1]] + + def last_item(self): + """ Get the item (key-value pair) for the highest key in the dict. """ + return self.__keys[-1], self.__couples[self.__keys[-1]] + + def keys(self): + """ Get an iterator to the keys in the dict. """ + return iter(self.__keys) + + def values(self): + """ Get an iterator to the values in the dict. """ + return (self.__couples[k] for k in self.__keys) + + def reversed_values(self): + """ Get an iterator to the values in the dict in reversed order or keys. """ + return (self.__couples[k] for k in reversed(self.__keys)) + + def items(self): + """ Get an iterator to the items in the dict. """ + return ((k, self.__couples[k]) for k in self.__keys) + + def sub_keys(self, key_from=None, key_to=None): + """ Return list of keys between key_from and key_to (both bounds included). """ + position_from, position_to = self._get_keys_interval(key_from, key_to) + return self.__keys[position_from:(position_to + 1)] + + def sub(self, key_from=None, key_to=None): + """ Return a list of values associated to keys between key_from and key_to (both bounds included). + + If key_from is None, lowest key in dict is used. + If key_to is None, greatest key in dict is used. + If key_from is not in dict, lowest key in dict greater than key_from is used. + If key_to is not in dict, greatest key in dict less than key_to is used. + + If dict is empty, return empty list. + With keys (None, None) return a copy of all values. + With keys (None, key_to), return values from first to the one associated to key_to. + With keys (key_from, None), return values from the one associated to key_from to the last value. + + :param key_from: start key + :param key_to: end key + :return: list: values in closed keys interval [key_from; key_to] + """ + position_from, position_to = self._get_keys_interval(key_from, key_to) + return [self.__couples[k] for k in self.__keys[position_from:(position_to + 1)]] + + def remove_sub(self, key_from=None, key_to=None): + """ Remove values associated to keys between key_from and key_to (both bounds included). + + See sub() doc about key_from and key_to. + + :param key_from: start key + :param key_to: end key + :return: nothing + """ + position_from, position_to = self._get_keys_interval(key_from, key_to) + keys_to_remove = self.__keys[position_from:(position_to + 1)] + for key in keys_to_remove: + self.remove(key) + + def key_from_index(self, index): + """ Return key matching given position in sorted dict, or None for invalid position. """ + return self.__keys[index] if -len(self.__keys) <= index < len(self.__keys) else None + + def get_previous_key(self, key): + """ Return greatest key lower than given key, or None if not exists. """ + return self.__keys.get_previous_value(key) + + def get_next_key(self, key): + """ Return smallest key greater then given key, or None if not exists. """ + return self.__keys.get_next_value(key) + + def _get_keys_interval(self, key_from, key_to): + """ Get a couple of internal key positions (index of key_from, index of key_to) allowing + to easily retrieve values in closed interval [index of key_from; index of key_to] + corresponding to Python slice [index of key_from : (index of key_to + 1)] + + If dict is empty, return (0, -1), so that python slice [0 : -1 + 1] corresponds to empty interval. + If key_from is None, lowest key in dict is used. + If key_to is None, greatest key in dict is used. + If key_from is not in dict, lowest key in dict greater than key_from is used. + If key_to is not in dict, greatest key in dict less than key_to is used. + + Thus: + - With keys (None, None), we get interval of all values. + - With keys (key_from, None), we get interval for values from key_from to the last key. + - With keys (None, key_to), we get interval for values from the first key to key_to. + + :param key_from: start key + :param key_to: end key + :return: (int, int): couple of integers: (index of key_from, index of key_to). + """ + if not self: + return 0, -1 + if key_from is not None and key_from not in self.__couples: + key_from = self.__keys.get_next_value(key_from) + if key_from is None: + return 0, -1 + if key_to is not None and key_to not in self.__couples: + key_to = self.__keys.get_previous_value(key_to) + if key_to is None: + return 0, -1 + if key_from is None and key_to is None: + key_from = self.first_key() + key_to = self.last_key() + elif key_from is not None and key_to is None: + key_to = self.last_key() + elif key_from is None and key_to is not None: + key_from = self.first_key() + if key_from > key_to: + raise IndexError('expected key_from <= key_to (%s vs %s)' % (key_from, key_to)) + position_from = self.__keys.index(key_from) + position_to = self.__keys.index(key_to) + assert position_from is not None and position_to is not None + return position_from, position_to + + def clear(self): + """ Remove all items from dict. """ + self.__couples.clear() + self.__keys.clear() + + def fill(self, dct): + """ Add given dict to this sorted dict. """ + if dct: + assert is_dictionary(dct) + for key, value in dct.items(): + self.put(key, value) + + def copy(self): + """ Return a copy of this sorted dict. """ + return SortedDict(self.__keys.element_type, self.__val_type, self.__couples) diff --git a/diplomacy/utils/sorted_set.py b/diplomacy/utils/sorted_set.py new file mode 100644 index 0000000..01cfb34 --- /dev/null +++ b/diplomacy/utils/sorted_set.py @@ -0,0 +1,157 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Sorted set implementation. """ +import bisect +from copy import copy + +from diplomacy.utils import exceptions +from diplomacy.utils.common import is_sequence + +class SortedSet(): + """ Sorted set (sorted values, each value appears once). """ + __slots__ = ('__type', '__list') + + def __init__(self, element_type, content=()): + """ Initialize a typed sorted set. + :param element_type: Expected type for values. + :param content: (optional) Sequence of values to initialize sorted set with. + """ + if not is_sequence(content): + raise exceptions.TypeException('sequence', type(content)) + self.__type = element_type + self.__list = [] + for element in content: + self.add(element) + + @staticmethod + def builder(element_type): + """ Return a function to build sorted sets from content (sequence of values). + :param element_type: expected type for sorted set values. + :return: callable + + Returned function expects a content parameter like SortedSet initializer. + builder_fn = SortedSet.builder(str) + my_sorted_set = builder_fn(['c', '3', 'p', '0']) + """ + return lambda iterable: SortedSet(element_type, iterable) + + @property + def element_type(self): + """ Get values type. """ + return self.__type + + def __str__(self): + """ String representation """ + return 'SortedSet(%s, %s)' % (self.__type.__name__, self.__list) + + def __len__(self): + """ Returns len of SortedSet """ + return len(self.__list) + + def __eq__(self, other): + """ Determines if 2 SortedSets are equal """ + assert isinstance(other, SortedSet) + return (self.element_type is other.element_type + and len(self) == len(other) + and all(a == b for a, b in zip(self, other))) + + def __getitem__(self, index): + """ Returns the item at the position index """ + return copy(self.__list[index]) + + def __iter__(self): + """ Returns an iterator """ + return self.__list.__iter__() + + def __reversed__(self): + """ Return reversed view of internal list. """ + return reversed(self.__list) + + def __contains__(self, element): + """ Determines if the element is in the SortedSet """ + assert isinstance(element, self.__type) + if self.__list: + position = bisect.bisect_left(self.__list, element) + return position != len(self.__list) and self.__list[position] == element + return False + + def add(self, element): + """ Add an element. """ + assert isinstance(element, self.__type) + if self.__list: + best_position = bisect.bisect_left(self.__list, element) + if best_position == len(self.__list): + self.__list.append(element) + elif self.__list[best_position] != element: + self.__list.insert(best_position, element) + else: + self.__list.append(element) + best_position = 0 + return best_position + + def get_next_value(self, element): + """ Get lowest value in sorted set greater than given element, or None if such values does not exists + in the sorted set. Given element may not exists in the sorted set. + """ + assert isinstance(element, self.__type) + if self.__list: + best_position = bisect.bisect_right(self.__list, element) + if best_position != len(self.__list): + if self.__list[best_position] != element: + return self.__list[best_position] + if best_position + 1 < len(self.__list): + return self.__list[best_position + 1] + return None + + def get_previous_value(self, element): + """ Get greatest value in sorted set less the given element, or None if such value does not exists + in the sorted set. Given element may not exists in the sorted set. + """ + assert isinstance(element, self.__type) + if self.__list: + best_position = bisect.bisect_left(self.__list, element) + if best_position == len(self.__list): + return self.__list[len(self.__list) - 1] + if best_position != 0: + return self.__list[best_position - 1] + return None + + def pop(self, index): + """ Remove and return value at given index. """ + return self.__list.pop(index) + + def remove(self, element): + """ Remove and return element. """ + assert isinstance(element, self.__type) + if self.__list: + position = bisect.bisect_left(self.__list, element) + if position != len(self.__list) and self.__list[position] == element: + return self.pop(position) + return None + + def index(self, element): + """ Return index of element in the set, or None if element is not in the set. """ + assert isinstance(element, self.__type) + if self.__list: + position = bisect.bisect_left(self.__list, element) + if position != len(self.__list) and self.__list[position] == element: + return position + return None + + def clear(self): + """ Remove all items from set. """ + self.__list.clear() diff --git a/diplomacy/utils/strings.py b/diplomacy/utils/strings.py new file mode 100644 index 0000000..2a74a03 --- /dev/null +++ b/diplomacy/utils/strings.py @@ -0,0 +1,236 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Some strings frequently used (to help prevent typos). """ + +ABBREV = 'abbrev' +ACTIVE = 'active' +ADJUST = 'adjust' +ADMIN = 'admin' +ADMINISTRATORS = 'administrators' +ALL_POSSIBLE_ORDERS = 'all_possible_orders' +ALLOCATE_COUNTRIES_RANDOMLY = 'allocate_countries_randomly' +ALLOW_MULTIPLE_COUNTRIES_PER_PLAYER = 'allow_multiple_countries_per_player' +ALLOW_NEW_PLAYERS_AFTER_START = 'allow_new_players_after_start' +ALLOW_OBSERVATIONS = 'allow_observations' +ALLOW_REGISTRATIONS = 'allow_registrations' +ALLOWED_VOTERS = 'allowed_voters' +ALWAYS_OMNISCIENT = 'always_omniscient' +AUTHENTICATION_TYPE = 'authentication_type' +AVAILABLE_MAPS = 'available_maps' +BACKUP_DELAY_SECONDS = 'backup_delay_seconds' +BODY = 'body' +BUFFER_SIZE = 'buffer_size' +CANCELED = 'canceled' +CENTERS = 'centers' +CHANNEL = 'channel' +CIVIL_DISORDER = 'civil_disorder' +CLEAR_INVALID_STATE_HISTORY = 'clear_invalid_state_history' +COMPLETED = 'completed' +CONTENT = 'content' +CONTROLLED_POWERS = 'controlled_powers' +CONTROLLER = 'controller' +COUNT_EXPECTED = 'count_expected' +COUNT_VOTED = 'count_voted' +CREATE_USER = 'create_user' +CURRENT_ORDER = 'current_order' +CURRENT_PHASE = 'current_phase' +CURRENT_PHASE_DATA = 'current_phase_data' +CURRENT_STATE = 'current_state' +CURRENT_TURN = 'current_turn' +DATA = 'data' +DEADLINE = 'deadline' +DEMOTE = 'demote' +DESC = 'desc' +DESIRED_COUNTRIES = 'desired_countries' +DUMMY = 'dummy' +DUMMY_PLAYER = 'dummy_player' +DUMMY_POWERS = 'dummy_powers' +ERROR = 'error' +FOR_OMNISCIENCE = 'for_omniscience' +FORCED = 'forced' +FORCED_ORDERS = 'forced_orders' +FORCED_WAIT_FLAG = 'forced_wait_flag' +FORMING = 'forming' +FROM_PHASE = 'from_phase' +FROM_TIMESTAMP = 'from_timestamp' +GAME = 'game' +GAME_ID = 'game_id' +GAME_PHASE = 'game_phase' +GAME_ROLE = 'game_role' +GAME_STARTED = 'game_started' +GAMES = 'games' +GLOBAL_MESSAGE_HISTORY = 'global_message_history' +GRADE = 'grade' +GRADE_UPDATE = 'grade_update' +HASHED = 'hashed' +HOMES = 'homes' +INCLUDE_PROTECTED = 'include_protected' +INFLUENCE = 'influence' +INITIAL_STATE = 'initial_state' +IS_DUMMY = 'is_dummy' +KICK_PLAYER = 'kick_player' +MAP_NAME = 'map_name' +MAP_POWERS = 'map_powers' +MAPS = 'maps' +MAPS_MTIME = 'maps_mtime' +MASTER_TYPE = 'master_type' +MAX_GAMES = 'max_games' +MESSAGE = 'message' +MESSAGE_HISTORY = 'message_history' +MESSAGES = 'messages' +META_RULES = 'meta_rules' +MODERATOR = 'moderator' +MODERATOR_USERNAMES = 'moderator_usernames' +MODERATORS = 'moderators' +N_CONTROLS = 'n_controls' +N_PLAYERS = 'n_players' +NAME = 'name' +NEUTRAL = 'neutral' +NO = 'no' +NO_RULES = 'no_rules' +NO_WAIT = 'no_wait' +NOTE = 'note' +NOTIFICATION = 'notification' +NOTIFICATION_ID = 'notification_id' +OBSERVER = 'observer' +OBSERVER_LEVEL = 'observer_level' +OBSERVER_NAME = 'observer_name' +OBSERVER_TYPE = 'observer_type' +OK = 'ok' +OMNISCIENT = 'omniscient' +OMNISCIENT_TYPE = 'omniscient_type' +OMNISCIENT_USERNAMES = 'omniscient_usernames' +ORDER = 'order' +ORDER_HISTORY = 'order_history' +ORDER_IS_SET = 'order_is_set' +ORDER_STATUS = 'order_status' +ORDERABLE_LOCATIONS = 'orderable_locations' +ORDERS = 'orders' +ORDERS_FINALIZED = 'orders_finalized' +ORDERS_STATUSES = 'orders_statuses' +OTHER_PLAYERS = 'other_players' +OUTCOME = 'outcome' +PARAMETERS = 'parameters' +PASSWORD = 'password' +PASSWORD_HASH = 'password_hash' +PAUSED = 'paused' +PHASE = 'phase' +PHASE_ABBR = 'phase_abbr' +PHASE_DATA = 'phase_data' +PHASE_DATA_TYPE = 'phase_data_type' +PING_SECONDS = 'ping_seconds' +PLAYER_ID = 'player_id' +PLAYERS = 'players' +POSSIBLE_ORDERS = 'possible_orders' +POWER = 'power' +POWER_FROM = 'power_from' +POWER_MESSAGE_HISTORY = 'power_message_history' +POWER_NAME = 'power_name' +POWER_NAMES = 'power_names' +POWER_NAMES_AGAINST = 'power_names_against' +POWER_NAMES_IN_FAVOUR = 'power_names_in_favour' +POWER_NAMES_NEUTRAL = 'power_names_neutral' +POWER_TO = 'power_to' +POWERS = 'powers' +POWERS_KICKED = 'powers_kicked' +PREVIOUS_PHASE = 'previous_phase' +PREVIOUS_PHASE_DATA = 'previous_phase_data' +PREVIOUS_STATE = 'previous_state' +PROMOTE = 'promote' +PROPOSAL = 'proposal' +RE_SENT = 're_sent' +REASON = 'reason' +RECIPIENT = 'recipient' +RECIPIENT_TOKEN = 'recipient_token' +REGISTER = 'register' +REGISTRATION_PASSWORD = 'registration_password' +REMOVE_CANCELED_GAMES = 'remove_canceled_games' +REMOVE_ENDED_GAMES = 'remove_ended_games' +REQUEST_ID = 'request_id' +RESULT = 'result' +RESULT_HISTORY = 'result_history' +RESULTS = 'results' +RETREATS = 'retreats' +ROLE = 'role' +RULES = 'rules' +SENDER = 'sender' +SERVER_TYPE = 'server_type' +SET_ORDER = 'set_order' +SETTINGS = 'settings' +START_AUTO = 'start_auto' +START_MASTER = 'start_master' +START_MODE = 'start_mode' +STATE = 'state' +STATE_HISTORY = 'state_history' +STATE_TYPE = 'state_type' +STATES = 'states' +STATUS = 'status' +SUPPLY_CENTERS = 'supply_centers' +TIME_SENT = 'time_sent' +TIMESTAMP = 'timestamp' +TIMESTAMP_CREATED = 'timestamp_created' +TIMESTAMP_SAVED = 'timestamp_saved' +TIMESTAMP_SYNC = 'timestamp_sync' +TIMESTAMPS = 'timestamps' +TO_PHASE = 'to_phase' +TO_TIMESTAMP = 'to_timestamp' +TOKEN = 'token' +TOKEN_TIMESTAMP = 'token_timestamp' +TOKEN_TO_USERNAME = 'token_to_username' +TOKENS = 'tokens' +TURN = 'turn' +TURN_HISTORY = 'turn_history' +UNDECIDED_PLAYER_MODE = 'undecided_player_mode' +UNITS = 'units' +USERNAME = 'username' +USERNAME_TO_TOKENS = 'username_to_tokens' +USERS = 'users' +VICTORY = 'victory' +VOTE = 'vote' +VOTE_ID = 'vote_id' +VOTE_IS_FORCED = 'vote_is_forced' +VOTES = 'votes' +WAIT = 'wait' +WIN = 'win' +WINNERS = 'winners' +WINTER_UNDECIDED_PLAYER_MODE = 'winter_undecided_player_mode' +YES = 'yes' +ZOBRIST_HASH = 'zobrist_hash' + +# Special name sets. +ALL_GAME_STATUSES = (FORMING, ACTIVE, PAUSED, COMPLETED, CANCELED) +ALL_GRADE_UPDATES = {PROMOTE, DEMOTE} +ALL_GRADES = {OMNISCIENT, ADMIN, MODERATOR} +ALL_COMM_LEVELS = {CHANNEL, GAME} +ALL_VOTE_DECISIONS = {YES, NO, NEUTRAL} +ALL_ROLE_TYPES = {OBSERVER_TYPE, OMNISCIENT_TYPE, SERVER_TYPE} +ALL_STATE_TYPES = {STATE_HISTORY, STATE, PHASE} + +def role_is_special(role): + """ Return True if role is a special role (observer or omniscient). """ + return role in {OBSERVER_TYPE, OMNISCIENT_TYPE} + +def switch_special_role(role): + """ Return opposite special role of given special role: + - observer role if omniscient role is given + - omniscient role if observer role is given + """ + if role == OBSERVER_TYPE: + return OMNISCIENT_TYPE + if role == OMNISCIENT_TYPE: + return OBSERVER_TYPE + raise ValueError('Unknown special role %s' % role) diff --git a/diplomacy/utils/tests/__init__.py b/diplomacy/utils/tests/__init__.py new file mode 100644 index 0000000..4f2769f --- /dev/null +++ b/diplomacy/utils/tests/__init__.py @@ -0,0 +1,16 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== diff --git a/diplomacy/utils/tests/test_common.py b/diplomacy/utils/tests/test_common.py new file mode 100644 index 0000000..a1c303d --- /dev/null +++ b/diplomacy/utils/tests/test_common.py @@ -0,0 +1,147 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Test for diplomacy network code utils. """ +import ujson as json + +from diplomacy.utils import common, exceptions + +def assert_raises(callback, expected_exceptions): + """ Checks that given callback raises given exceptions. """ + + try: + callback() + except expected_exceptions: + pass + else: + raise AssertionError('Should fail %s %s' % (callback, str(expected_exceptions))) + +def assert_equals(expected, computed): + """ Checks that expect == computed. """ + + if expected != computed: + raise AssertionError('\nExpected:\n=========\n%s\n\nComputed:\n=========\n%s\n' % (expected, computed)) + +def test_hash_password(): + """ Test passwords hashing. Note: slower than the other tests. """ + + password1 = '123456789' + password2 = 'abcdef' + password_unicode = 'しろいねこをみた。 白い猫を見た。' + for password in (password1, password2, password_unicode): + hashed_password = common.hash_password(password) + json_hashed_password = json.dumps(common.hash_password(password)) + hashed_password_from_json = json.loads(json_hashed_password) + # It seems hashed passwords are not necessarily the same for 2 different calls to hash function. + assert common.is_valid_password(password, hashed_password), (password, hashed_password) + assert common.is_valid_password(password, hashed_password_from_json), (password, hashed_password_from_json) + +def test_generate_token(): + """ Test token generation. """ + + for n_bytes in (128, 344): + token = common.generate_token(n_bytes) + assert isinstance(token, str) and len(token) == 2 * n_bytes + +def test_is_sequence(): + """ Test sequence type checking function. """ + + assert common.is_sequence((1, 2, 3)) + assert common.is_sequence([1, 2, 3]) + assert common.is_sequence({1, 2, 3}) + assert common.is_sequence(()) + assert common.is_sequence([]) + assert common.is_sequence(set()) + assert not common.is_sequence('i am a string') + assert not common.is_sequence({}) + assert not common.is_sequence(1) + assert not common.is_sequence(False) + assert not common.is_sequence(-2.5) + +def test_is_dictionary(): + """ Test dictionary type checking function. """ + + assert common.is_dictionary({'a': 1, 'b': 2}) + assert not common.is_dictionary((1, 2, 3)) + assert not common.is_dictionary([1, 2, 3]) + assert not common.is_dictionary({1, 2, 3}) + + assert not common.is_dictionary(()) + assert not common.is_dictionary([]) + assert not common.is_dictionary(set()) + + assert not common.is_dictionary('i am a string') + +def test_camel_to_snake_case(): + """ Test conversion from camel case to snake case. """ + + for camel, expected_snake in [ + ('a', 'a'), + ('A', 'a'), + ('AA', 'a_a'), + ('AbCdEEf', 'ab_cd_e_ef'), + ('Aa', 'aa'), + ('OnGameDone', 'on_game_done'), + ('AbstractSuperClass', 'abstract_super_class'), + ('ABCDEFghikKLm', 'a_b_c_d_e_fghik_k_lm'), + ('is_a_thing', 'is_a_thing'), + ('A_a_Aa__', 'a_a_aa__'), + ('Horrible_SuperClass_nameWith_mixedSyntax', 'horrible_super_class_name_with_mixed_syntax'), + ]: + computed_snake = common.camel_case_to_snake_case(camel) + assert computed_snake == expected_snake, ('camel : expected : computed:', camel, expected_snake, computed_snake) + +def test_snake_to_camel_case(): + """ Test conversion from snake case to camel upper case. """ + + for expected_camel, snake in [ + ('A', 'a'), + ('AA', 'a_a'), + ('AbCdEEf', 'ab_cd_e_ef'), + ('Aa', 'aa'), + ('OnGameDone', 'on_game_done'), + ('AbstractSuperClass', 'abstract_super_class'), + ('ABCDEFghikKLm', 'a_b_c_d_e_fghik_k_lm'), + ('IsAThing', 'is_a_thing'), + ('AAAa__', 'a_a_aa__'), + ('_AnHorrible_ClassName', '__an_horrible__class_name'), + ]: + computed_camel = common.snake_case_to_upper_camel_case(snake) + assert computed_camel == expected_camel, ('snake : expected : computed:', snake, expected_camel, computed_camel) + +def test_assert_no_common_keys(): + """ Test dictionary disjunction checking function. """ + + dct1 = {'a': 1, 'b': 2, 'c': 3} + dct2 = {'a': 1, 'e': 2, 'd': 3} + dct3 = {'m': 1, 'e': 2, 'f': 3} + common.assert_no_common_keys(dct1, dct3) + assert_raises(lambda: common.assert_no_common_keys(dct1, dct2), exceptions.CommonKeyException) + assert_raises(lambda: common.assert_no_common_keys(dct2, dct3), exceptions.CommonKeyException) + +def test_timestamp(): + """ Test timestamp generation. """ + + timestamp1 = common.timestamp_microseconds() + timestamp2 = common.timestamp_microseconds() + timestamp3 = common.timestamp_microseconds() + assert isinstance(timestamp1, int) + assert isinstance(timestamp2, int) + assert isinstance(timestamp3, int) + assert timestamp1 > 1e6 + assert timestamp2 > 1e6 + assert timestamp3 > 1e6 + assert timestamp1 <= timestamp2 <= timestamp3, (timestamp1, timestamp2, timestamp3) diff --git a/diplomacy/utils/tests/test_jsonable.py b/diplomacy/utils/tests/test_jsonable.py new file mode 100644 index 0000000..73d65c1 --- /dev/null +++ b/diplomacy/utils/tests/test_jsonable.py @@ -0,0 +1,81 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Test Jsonable. """ +import ujson as json + +from diplomacy.utils import parsing +from diplomacy.utils.jsonable import Jsonable +from diplomacy.utils.sorted_dict import SortedDict +from diplomacy.utils.sorted_set import SortedSet + +class MyJsonable(Jsonable): + """ Example of class derived from Jsonable. """ + __slots__ = ('field_a', 'field_b', 'field_c', 'field_d', 'field_e', 'field_f', 'field_g') + + model = { + 'field_a': bool, + 'field_b': str, + 'field_c': parsing.OptionalValueType(float), + 'field_d': parsing.DefaultValueType(str, 'super'), + 'field_e': parsing.SequenceType(int), + 'field_f': parsing.SequenceType(float, sequence_builder=SortedSet.builder(float)), + 'field_g': parsing.DefaultValueType(parsing.DictType(str, int, SortedDict.builder(str, int)), {'x': -1}) + } + + def __init__(self, **kwargs): + """ Constructor """ + self.field_a = None + self.field_b = None + self.field_c = None + self.field_d = None + self.field_e = None + self.field_f = None + self.field_g = {} + super(MyJsonable, self).__init__(**kwargs) + +def test_jsonable_parsing(): + """ Test parsing for Jsonable. """ + + attributes = ('field_a', 'field_b', 'field_c', 'field_d', 'field_e', 'field_f', 'field_g') + + # Building and validating + my_jsonable = MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5]) + for attribute_name in attributes: + assert hasattr(my_jsonable, attribute_name) + assert isinstance(my_jsonable.field_a, bool) + assert isinstance(my_jsonable.field_b, str) + assert my_jsonable.field_c is None + assert isinstance(my_jsonable.field_d, str), my_jsonable.field_d + assert isinstance(my_jsonable.field_e, list) + assert isinstance(my_jsonable.field_f, SortedSet) + assert isinstance(my_jsonable.field_g, SortedDict) + assert my_jsonable.field_d == 'super' + assert my_jsonable.field_e == [1] + assert my_jsonable.field_f == SortedSet(float, (6.5,)) + assert len(my_jsonable.field_g) == 1 and my_jsonable.field_g['x'] == -1 + + # Building from its json representation and validating + from_json = MyJsonable.from_dict(json.loads(json.dumps(my_jsonable.to_dict()))) + for attribute_name in attributes: + assert hasattr(from_json, attribute_name), attribute_name + assert from_json.field_a == my_jsonable.field_a + assert from_json.field_b == my_jsonable.field_b + assert from_json.field_c == my_jsonable.field_c + assert from_json.field_d == my_jsonable.field_d + assert from_json.field_e == my_jsonable.field_e + assert from_json.field_f == my_jsonable.field_f + assert from_json.field_g == my_jsonable.field_g diff --git a/diplomacy/utils/tests/test_jsonable_changes.py b/diplomacy/utils/tests/test_jsonable_changes.py new file mode 100644 index 0000000..992a8fb --- /dev/null +++ b/diplomacy/utils/tests/test_jsonable_changes.py @@ -0,0 +1,189 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Test changes in a Jsonable schema. """ +#pylint: disable=invalid-name +from diplomacy.utils import parsing +from diplomacy.utils.jsonable import Jsonable + +def converter_to_int(val): + """ A converter from given value to an integer. Used in Version1. """ + try: + return int(val) + except ValueError: + return 0 + +class Version1(Jsonable): + """ A Jsonable with fields a, b, c, d. + NB: To parse a dict from Version22 to Version1, modified fields a and c must be convertible in Version1. + using ConverterType in Version1. + """ + model = { + 'a': parsing.ConverterType(int, converter_to_int), + 'b': parsing.OptionalValueType(str), + 'c': parsing.ConverterType(float, converter_function=float), + 'd': parsing.DefaultValueType(bool, True), + } + + def __init__(self, **kwargs): + self.a = None + self.b = None + self.c = None + self.d = None + super(Version1, self).__init__(**kwargs) + +class Version20(Jsonable): + """ Version1 with removed fields b and d. + NB: To parse a dict from Version20 to Version1, removed fields b and d must be optional in Version1. + """ + model = { + 'a': int, + 'c': float, + } + + def __init__(self, **kwargs): + self.a = None + self.c = None + super(Version20, self).__init__(**kwargs) + +class Version21(Jsonable): + """ Version1 with added fields e and f. + NB: To parse a dict from Version1 to Version21, added fields e and f must be optional in Version21. + """ + model = { + 'a': int, + 'b': str, + 'c': float, + 'd': bool, + 'e': parsing.DefaultValueType(parsing.EnumerationType([100, 200, 300, 400]), 100), + 'f': parsing.DefaultValueType(dict, {'x': 1, 'y': 2}) + } + + def __init__(self, **kwargs): + self.a = None + self.b = None + self.c = None + self.d = None + self.e = None + self.f = {} + super(Version21, self).__init__(**kwargs) + +class Version22(Jsonable): + """ Version1 with modified types for a and c. + NB: To parse a dict from Version1 to Version22, modified fields a and c must be convertible + using ConverterType in Version22. + """ + model = { + 'a': parsing.ConverterType(str, converter_function=str), + 'b': str, + 'c': parsing.ConverterType(bool, converter_function=bool), + 'd': bool, + } + + def __init__(self, **kwargs): + self.a = None + self.b = None + self.c = None + self.d = None + super(Version22, self).__init__(**kwargs) + +class Version3(Jsonable): + """ Version 1 with a modified, b removed, e added. + To parse a dict between Version3 and Version1: + - a must be convertible in both versions. + - b must be optional in Version1. + - e must be optional in Version3. + """ + model = { + 'a': parsing.ConverterType(str, converter_function=str), + 'c': float, + 'd': bool, + 'e': parsing.OptionalValueType(parsing.SequenceType(int)) + } + + def __init__(self, **kwargs): + self.a = None + self.c = None + self.d = None + self.e = None + super(Version3, self).__init__(**kwargs) + +def test_jsonable_changes_v1_v20(): + """ Test changes from Version1 to Version20. """ + v20 = Version20(a=1, c=1.5) + v1 = Version1(a=1, b='b', c=1.5, d=False) + json_v1 = v1.to_dict() + v20_from_v1 = Version20.from_dict(json_v1) + json_v20_from_v1 = v20_from_v1.to_dict() + v1_from_v20_from_v1 = Version1.from_dict(json_v20_from_v1) + assert v1_from_v20_from_v1.b is None + assert v1_from_v20_from_v1.d is True + json_v20 = v20.to_dict() + v1_from_v20 = Version1.from_dict(json_v20) + assert v1_from_v20.b is None + assert v1_from_v20.d is True + +def test_jsonable_changes_v1_v21(): + """ Test changes from Version1 to Version21. """ + v21 = Version21(a=1, b='b21', c=1.5, d=True, e=300, f=dict(x=1, y=2)) + v1 = Version1(a=1, b='b', c=1.5, d=False) + json_v1 = v1.to_dict() + v21_from_v1 = Version21.from_dict(json_v1) + assert v21_from_v1.e == 100 + assert v21_from_v1.f['x'] == 1 + assert v21_from_v1.f['y'] == 2 + json_v21_from_v1 = v21_from_v1.to_dict() + v1_from_v21_from_v1 = Version1.from_dict(json_v21_from_v1) + assert v1_from_v21_from_v1.b == 'b' + assert v1_from_v21_from_v1.d is False + json_v21 = v21.to_dict() + v1_from_v21 = Version1.from_dict(json_v21) + assert v1_from_v21.b == 'b21' + assert v1_from_v21.d is True + +def test_jsonable_changes_v1_v22(): + """ Test changes from Version1 to Version22. """ + v22 = Version22(a='a', b='b', c=False, d=False) + v1 = Version1(a=1, b='b', c=1.5, d=False) + json_v1 = v1.to_dict() + v22_from_v1 = Version22.from_dict(json_v1) + assert v22_from_v1.a == '1' + assert v22_from_v1.c is True + json_v22_from_v1 = v22_from_v1.to_dict() + v1_from_v22_from_v1 = Version1.from_dict(json_v22_from_v1) + assert v1_from_v22_from_v1.a == 1 + assert v1_from_v22_from_v1.c == 1.0 + json_v22 = v22.to_dict() + v1_from_v22 = Version1.from_dict(json_v22) + assert v1_from_v22.a == 0 + assert v1_from_v22.c == 0.0 + +def test_jsonable_changes_v1_v3(): + """ Test changes from Version1 to Version3. """ + v3 = Version3(a='a', c=1.5, d=False, e=(1, 2, 3)) + v1 = Version1(a=1, b='b', c=1.5, d=False) + json_v1 = v1.to_dict() + v3_from_v1 = Version3.from_dict(json_v1) + assert v3_from_v1.a == '1' + assert v3_from_v1.e is None + json_v3_from_v1 = v3_from_v1.to_dict() + v1_from_v3_from_v1 = Version1.from_dict(json_v3_from_v1) + assert v1_from_v3_from_v1.a == 1 + assert v1_from_v3_from_v1.b is None + json_v3 = v3.to_dict() + v1_from_v3 = Version1.from_dict(json_v3) + assert v1_from_v3.a == 0 + assert v1_from_v3.b is None diff --git a/diplomacy/utils/tests/test_parsing.py b/diplomacy/utils/tests/test_parsing.py new file mode 100644 index 0000000..f64ad26 --- /dev/null +++ b/diplomacy/utils/tests/test_parsing.py @@ -0,0 +1,307 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Test module parsing. """ +from diplomacy.utils import exceptions, parsing +from diplomacy.utils.sorted_dict import SortedDict +from diplomacy.utils.sorted_set import SortedSet +from diplomacy.utils.tests.test_common import assert_raises +from diplomacy.utils.tests.test_jsonable import MyJsonable + +class MyStringable(): + """ Example of Stringable class. + As instances of such class may be used as dict keys, class should define a proper __hash__(). + """ + + def __init__(self, value): + self.attribute = str(value) + + def __str__(self): + return 'MyStringable %s' % self.attribute + + def __hash__(self): + return hash(self.attribute) + + def __eq__(self, other): + return isinstance(other, MyStringable) and self.attribute == other.attribute + + def __lt__(self, other): + return isinstance(other, MyStringable) and self.attribute < other.attribute + + @staticmethod + def from_string(str_repr): + """ Converts a string representation `str_repr` of MyStringable to an instance of MyStringable. """ + return MyStringable(str_repr[len('MyStringable '):]) + +def test_default_value_type(): + """ Test default value type. """ + + for default_value in (True, False, None): + checker = parsing.DefaultValueType(bool, default_value) + assert_raises(lambda ch=checker: ch.validate(1), exceptions.TypeException) + assert_raises(lambda ch=checker: ch.validate(1.1), exceptions.TypeException) + assert_raises(lambda ch=checker: ch.validate(''), exceptions.TypeException) + for value in (True, False, None): + checker.validate(value) + if value is None: + assert checker.to_type(value) is default_value + assert checker.to_json(value) is default_value + else: + assert checker.to_type(value) is value + assert checker.to_json(value) is value + assert checker.update(None) is default_value + +def test_optional_value_type(): + """ Test optional value type. """ + + checker = parsing.OptionalValueType(bool) + assert_raises(lambda ch=checker: ch.validate(1), exceptions.TypeException) + assert_raises(lambda ch=checker: ch.validate(1.1), exceptions.TypeException) + assert_raises(lambda ch=checker: ch.validate(''), exceptions.TypeException) + for value in (True, False, None): + checker.validate(value) + assert checker.to_type(value) is value + assert checker.to_json(value) is value + assert checker.update(None) is None + +def test_sequence_type(): + """ Test sequence type. """ + + # With default sequence builder. + checker = parsing.SequenceType(int) + checker.validate((1, 2, 3)) + checker.validate([1, 2, 3]) + checker.validate({1, 2, 3}) + checker.validate(SortedSet(int)) + checker.validate(SortedSet(int, (1, 2, 3))) + assert_raises(lambda: checker.validate((1, 2, 3.0)), exceptions.TypeException) + assert_raises(lambda: checker.validate((1.0, 2.0, 3.0)), exceptions.TypeException) + assert isinstance(checker.to_type((1, 2, 3)), list) + # With SortedSet as sequence builder. + checker = parsing.SequenceType(float) + checker.validate((1.0, 2.0, 3.0)) + checker.validate([1.0, 2.0, 3.0]) + checker.validate({1.0, 2.0, 3.0}) + assert_raises(lambda: checker.validate((1, 2, 3.0)), exceptions.TypeException) + assert_raises(lambda: checker.validate((1.0, 2.0, 3)), exceptions.TypeException) + checker = parsing.SequenceType(int, sequence_builder=SortedSet.builder(int)) + initial_list = (1, 2, 7, 7, 1) + checker.validate(initial_list) + updated_list = checker.update(initial_list) + assert isinstance(updated_list, SortedSet) and updated_list.element_type is int + assert updated_list == SortedSet(int, (1, 2, 7)) + assert checker.to_json(updated_list) == [1, 2, 7] + assert checker.to_type([7, 2, 1, 1, 7, 1, 7]) == updated_list + +def test_jsonable_class_type(): + """ Test parser for Jsonable sub-classes. """ + + checker = parsing.JsonableClassType(MyJsonable) + my_jsonable = MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5]) + my_jsonable_dict = { + 'field_a': False, + 'field_b': 'test', + 'field_e': (1, 2), + 'field_f': (1.0, 2.0), + } + checker.validate(my_jsonable) + assert_raises(lambda: checker.validate(None), exceptions.TypeException) + assert_raises(lambda: checker.validate(my_jsonable_dict), exceptions.TypeException) + +def test_stringable_type(): + """ Test stringable type. """ + + checker = parsing.StringableType(str) + checker.validate('0') + checker = parsing.StringableType(MyStringable) + checker.validate(MyStringable('test')) + assert_raises(lambda: checker.validate('test'), exceptions.TypeException) + assert_raises(lambda: checker.validate(None), exceptions.TypeException) + +def test_dict_type(): + """ Test dict type. """ + + checker = parsing.DictType(str, int) + checker.validate({'test': 1}) + assert_raises(lambda: checker.validate({'test': 1.0}), exceptions.TypeException) + checker = parsing.DictType(MyStringable, float) + checker.validate({MyStringable('12'): 2.5}) + assert_raises(lambda: checker.validate({'12': 2.5}), exceptions.TypeException) + assert_raises(lambda: checker.validate({MyStringable('12'): 2}), exceptions.TypeException) + checker = parsing.DictType(MyStringable, float, dict_builder=SortedDict.builder(MyStringable, float)) + value = {MyStringable(12): 22.0} + checker.validate(value) + updated_value = checker.update(value) + assert isinstance(updated_value, SortedDict) + assert updated_value.key_type is MyStringable + assert updated_value.val_type is float + json_value = {'MyStringable 12': 22.0} + assert checker.to_type(json_value) == SortedDict(MyStringable, float, {MyStringable('12'): 22.0}) + assert checker.to_json(SortedDict(MyStringable, float, {MyStringable(12): 22.0})) == json_value + +def test_sequence_of_values_type(): + """ Test parser for sequence of allowed values. """ + + checker = parsing.EnumerationType({'a', 'b', 'c', 'd'}) + checker.validate('d') + checker.validate('c') + checker.validate('b') + checker.validate('a') + assert_raises(lambda: checker.validate('e'), exceptions.ValueException) + +def test_sequence_of_primitives_type(): + """ Test parser for sequence of primitive types. """ + + checker = parsing.SequenceOfPrimitivesType((int, bool)) + checker.validate(False) + checker.validate(True) + checker.validate(0) + checker.validate(1) + assert_raises(lambda: checker.validate(0.0), exceptions.TypeException) + assert_raises(lambda: checker.validate(1.0), exceptions.TypeException) + assert_raises(lambda: checker.validate(''), exceptions.TypeException) + assert_raises(lambda: checker.validate('a non-empty string'), exceptions.TypeException) + +def test_primitive_type(): + """ Test parser for primitive type. """ + + checker = parsing.PrimitiveType(bool) + checker.validate(True) + checker.validate(False) + assert_raises(lambda: checker.validate(None), exceptions.TypeException) + assert_raises(lambda: checker.validate(0), exceptions.TypeException) + assert_raises(lambda: checker.validate(1), exceptions.TypeException) + assert_raises(lambda: checker.validate(''), exceptions.TypeException) + assert_raises(lambda: checker.validate('a non-empty string'), exceptions.TypeException) + +def test_model_parsing(): + """ Test parsing for a real model. """ + + model = { + 'name': str, + 'language': ('fr', 'en'), + 'myjsonable': parsing.JsonableClassType(MyJsonable), + 'mydict': parsing.DictType(str, float), + 'nothing': (bool, str), + 'default_float': parsing.DefaultValueType(float, 33.44), + 'optional_float': parsing.OptionalValueType(float) + } + bad_data_field = { + '_name_': 'hello', + 'language': 'fr', + 'myjsonable': MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5]), + 'mydict': { + 'a': 2.5, + 'b': -1.6 + }, + 'nothing': 'thanks' + } + bad_data_type = { + 'name': 'hello', + 'language': 'fr', + 'myjsonable': MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5]), + 'mydict': { + 'a': 2.5, + 'b': -1.6 + }, + 'nothing': 2.5 + } + bad_data_value = { + 'name': 'hello', + 'language': '__', + 'myjsonable': MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5]), + 'mydict': { + 'a': 2.5, + 'b': -1.6 + }, + 'nothing': '2.5' + } + good_data = { + 'name': 'hello', + 'language': 'fr', + 'myjsonable': MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5]), + 'mydict': { + 'a': 2.5, + 'b': -1.6 + }, + 'nothing': '2.5' + } + assert_raises(lambda: parsing.validate_data(bad_data_field, model), (exceptions.TypeException,)) + assert_raises(lambda: parsing.validate_data(bad_data_type, model), (exceptions.TypeException,)) + assert_raises(lambda: parsing.validate_data(bad_data_value, model), (exceptions.ValueException,)) + + assert 'default_float' not in good_data + assert 'optional_float' not in good_data + parsing.validate_data(good_data, model) + updated_good_data = parsing.update_data(good_data, model) + assert 'default_float' in updated_good_data + assert 'optional_float' in updated_good_data + assert updated_good_data['default_float'] == 33.44 + assert updated_good_data['optional_float'] is None + +def test_converter_type(): + """ Test parser converter type. """ + + def converter_to_int(val): + """ Converts value to integer """ + try: + return int(val) + except (ValueError, TypeError): + return 0 + + checker = parsing.ConverterType(str, converter_function=lambda val: 'String of %s' % val) + checker.validate('a string') + checker.validate(10) + checker.validate(True) + checker.validate(None) + checker.validate(-2.5) + assert checker.update(10) == 'String of 10' + assert checker.update(False) == 'String of False' + assert checker.update('string') == 'String of string' + checker = parsing.ConverterType(int, converter_function=converter_to_int) + checker.validate(10) + checker.validate(True) + checker.validate(None) + checker.validate(-2.5) + assert checker.update(10) == 10 + assert checker.update(True) == 1 + assert checker.update(False) == 0 + assert checker.update(-2.5) == -2 + assert checker.update('44') == 44 + assert checker.update('a') == 0 + +def test_indexed_sequence(): + """ Test parser type for dicts stored as sequences. """ + checker = parsing.IndexedSequenceType(parsing.DictType(str, parsing.JsonableClassType(MyJsonable)), 'field_b') + sequence = [ + MyJsonable(field_a=True, field_b='x1', field_e=[1, 2, 3], field_f=[1., 2., 3.]), + MyJsonable(field_a=True, field_b='x3', field_e=[1, 2, 3], field_f=[1., 2., 3.]), + MyJsonable(field_a=True, field_b='x2', field_e=[1, 2, 3], field_f=[1., 2., 3.]), + MyJsonable(field_a=True, field_b='x5', field_e=[1, 2, 3], field_f=[1., 2., 3.]), + MyJsonable(field_a=True, field_b='x4', field_e=[1, 2, 3], field_f=[1., 2., 3.]) + ] + dct = {element.field_b: element for element in sequence} + checker.validate(dct) + checker.update(dct) + jval = checker.to_json(dct) + assert isinstance(jval, list), type(jval) + from_jval = checker.to_type(jval) + assert isinstance(from_jval, dict), type(from_jval) + assert len(dct) == 5 + assert len(from_jval) == 5 + for key in ('x1', 'x2', 'x3', 'x4', 'x5'): + assert key in dct, (key, list(dct.keys())) + assert key in from_jval, (key, list(from_jval.keys())) diff --git a/diplomacy/utils/tests/test_priority_dict.py b/diplomacy/utils/tests/test_priority_dict.py new file mode 100644 index 0000000..cb7023d --- /dev/null +++ b/diplomacy/utils/tests/test_priority_dict.py @@ -0,0 +1,102 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Test class PriorityDict. """ +from diplomacy.utils.priority_dict import PriorityDict +from diplomacy.utils.tests.test_common import assert_equals + +def test_priority_dict(): + """ Test Heap class PriorityDict. """ + + for unordered_list in [ + [464, 21, 43453, 211, 324, 321, 102, 1211, 14, 875, 1, 33444, 22], + 'once upon a time in West'.split(), + 'This is a sentence with many words like panthera, lion, tiger, cat or cheetah!'.split() + ]: + expected_ordered_set = list(sorted(set(unordered_list))) + computed_sorted_list = [] + priority_dict = PriorityDict() + for element in unordered_list: + priority_dict[element] = element + while priority_dict: + value, key = priority_dict.smallest() + computed_sorted_list.append(value) + del priority_dict[key] + assert_equals(expected_ordered_set, computed_sorted_list) + +def test_item_getter_setter_deletion(): + """ Test PriorityDict item setter/getter/deletion. """ + + priority_dict = PriorityDict() + priority_dict['a'] = 12 + priority_dict['f'] = 9 + priority_dict['b'] = 23 + assert list(priority_dict.keys()) == ['f', 'a', 'b'] + assert priority_dict['a'] == 12 + assert priority_dict['f'] == 9 + assert priority_dict['b'] == 23 + priority_dict['e'] = -1 + priority_dict['a'] = 8 + del priority_dict['b'] + assert list(priority_dict.keys()) == ['e', 'a', 'f'] + assert list(priority_dict.values()) == [-1, 8, 9] + +def test_iterations(): + """ test iterations: + - for key in priority_dict + - priority_dict.keys() + - priority_dict.values() + - priority_dict.items() + """ + priorities = [464, 21, 43453, 211, 324, 321, 102, 1211, 14, 875, 1, 33444, 22] + + # Build priority dict. + priority_dict = PriorityDict() + for priority in priorities: + priority_dict['value of %s' % priority] = priority + + # Build expected priorities and keys. + expected_sorted_priorities = list(sorted(priorities)) + expected_sorted_keys = ['value of %s' % priority for priority in sorted(priorities)] + + # Iterate on priority dict. + computed_sorted_priorities = [priority_dict[key] for key in priority_dict] + # Iterate on priority dict keys. + sorted_priorities_from_key = [priority_dict[key] for key in priority_dict.keys()] + # Iterate on priority dict values. + sorted_priorities_from_values = list(priority_dict.values()) + # Iterate on priority dict items. + priority_dict_items = list(priority_dict.items()) + # Get priority dict keys. + priority_dict_keys = list(priority_dict.keys()) + # Get priority dict keys from items (to validate items). + priority_dict_keys_from_items = [item[0] for item in priority_dict_items] + # Get priority dict values from items (to validate items). + priority_dict_values_from_items = [item[1] for item in priority_dict_items] + + for expected, computed in [ + (expected_sorted_priorities, computed_sorted_priorities), + (expected_sorted_priorities, sorted_priorities_from_key), + (expected_sorted_priorities, sorted_priorities_from_values), + (expected_sorted_priorities, priority_dict_values_from_items), + (expected_sorted_keys, priority_dict_keys_from_items), + (expected_sorted_keys, priority_dict_keys), + ]: + assert_equals(expected, computed) + + # Priority dict should have not been modified. + assert_equals(len(priorities), len(priority_dict)) + assert all(key in priority_dict for key in expected_sorted_keys) diff --git a/diplomacy/utils/tests/test_sorted_dict.py b/diplomacy/utils/tests/test_sorted_dict.py new file mode 100644 index 0000000..559c36d --- /dev/null +++ b/diplomacy/utils/tests/test_sorted_dict.py @@ -0,0 +1,154 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Test class SortedDict. """ +from diplomacy.utils import common +from diplomacy.utils.sorted_dict import SortedDict +from diplomacy.utils.tests.test_common import assert_equals + +def test_init_bool_and_len(): + """ Test SortedDict initialization, length and conversion to boolean. """ + + sorted_dict = SortedDict(int, str) + assert not sorted_dict + sorted_dict = SortedDict(int, str, {2: 'two', 4: 'four', 99: 'ninety-nine'}) + assert sorted_dict + assert len(sorted_dict) == 3 + +def test_builder_and_properties(): + """ Test SortedDict builder and properties key_type and val_type. """ + + builder_float_to_bool = SortedDict.builder(float, bool) + sorted_dict = builder_float_to_bool({2.5: True, 2.7: False, 2.9: True}) + assert isinstance(sorted_dict, SortedDict) and sorted_dict.key_type is float and sorted_dict.val_type is bool + +def test_items_functions(): + """ Test SortedDict item setter/getter and methods put() and __contains__(). """ + + expected_keys = ['cat', 'lion', 'panthera', 'serval', 'tiger'] + sorted_dict = SortedDict(str, float, {'lion': 1.5, 'tiger': -2.7}) + # Test setter. + sorted_dict['panthera'] = -.88 + sorted_dict['cat'] = 2223. + # Test put(). + sorted_dict.put('serval', 39e12) + # Test __contains__. + assert 'lions' not in sorted_dict + assert all(key in sorted_dict for key in expected_keys) + # Test getter. + assert sorted_dict['cat'] == 2223. + assert sorted_dict['serval'] == 39e12 + # Test setter then getter. + assert sorted_dict['lion'] == 1.5 + sorted_dict['lion'] = 2.3 + assert sorted_dict['lion'] == 2.3 + # Test get, + assert sorted_dict.get('lions') is None + assert sorted_dict.get('lion') == 2.3 + +def test_item_deletion_and_remove(): + """ Test SortedDict methods remove() and __delitem__(). """ + + sorted_dict = SortedDict(str, float, {'lion': 1.5, 'tiger': -2.7, 'panthera': -.88, 'cat': 2223., 'serval': 39e12}) + assert len(sorted_dict) == 5 + assert 'serval' in sorted_dict + sorted_dict.remove('serval') + assert len(sorted_dict) == 4 + assert 'serval' not in sorted_dict + removed = sorted_dict.remove('tiger') + assert len(sorted_dict) == 3 + assert 'tiger' not in sorted_dict + assert removed == -2.7 + assert sorted_dict.remove('tiger') is None + assert sorted_dict.remove('key not in dict') is None + del sorted_dict['panthera'] + assert len(sorted_dict) == 2 + assert 'panthera' not in sorted_dict + assert 'cat' in sorted_dict + assert 'lion' in sorted_dict + +def test_iterations(): + """ Test SortedDict iterations (for key in dict, keys(), values(), items()). """ + + expected_sorted_keys = ['cat', 'lion', 'panthera', 'serval', 'tiger'] + expected_sorted_values = [2223., 1.5, -.88, 39e12, -2.7] + sorted_dict = SortedDict(str, float, {'lion': 1.5, 'tiger': -2.7, 'panthera': -.88, 'cat': 2223., 'serval': 39e12}) + computed_sorted_keys = [key for key in sorted_dict] + computed_sorted_keys_from_keys = list(sorted_dict.keys()) + computed_sorted_values = list(sorted_dict.values()) + keys_from_items = [] + values_from_items = [] + for key, value in sorted_dict.items(): + keys_from_items.append(key) + values_from_items.append(value) + assert_equals(expected_sorted_keys, computed_sorted_keys) + assert_equals(expected_sorted_keys, computed_sorted_keys_from_keys) + assert_equals(expected_sorted_keys, keys_from_items) + assert_equals(expected_sorted_values, values_from_items) + assert_equals(expected_sorted_values, computed_sorted_values) + +def test_bound_keys_getters(): + """ Test SortedDict methods first_key(), last_key(), last_value(), last_item(), + get_previous_key(), get_next_key(). + """ + + sorted_dict = SortedDict(str, float, {'lion': 1.5, 'tiger': -2.7}) + sorted_dict['panthera'] = -.88 + sorted_dict['cat'] = 2223. + sorted_dict['serval'] = 39e12 + assert sorted_dict.first_key() == 'cat' + assert sorted_dict.last_key() == 'tiger' + assert sorted_dict.last_value() == sorted_dict['tiger'] == -2.7 + assert sorted_dict.last_item() == ('tiger', -2.7) + assert sorted_dict.get_previous_key('cat') is None + assert sorted_dict.get_next_key('cat') == 'lion' + assert sorted_dict.get_previous_key('tiger') == 'serval' + assert sorted_dict.get_next_key('tiger') is None + assert sorted_dict.get_previous_key('panthera') == 'lion' + assert sorted_dict.get_next_key('panthera') == 'serval' + +def test_equality(): + """ Test SortedDict equality. """ + + empty_sorted_dict_float_int = SortedDict(float, int) + empty_sorted_dict_float_bool_1 = SortedDict(float, bool) + empty_sorted_dict_float_bool_2 = SortedDict(float, bool) + sorted_dict_float_int_1 = SortedDict(float, int, {2.5: 17, 3.3: 49, -5.7: 71}) + sorted_dict_float_int_2 = SortedDict(float, int, {2.5: 17, 3.3: 49, -5.7: 71}) + sorted_dict_float_int_3 = SortedDict(float, int, {2.5: -17, 3.3: 49, -5.7: 71}) + assert empty_sorted_dict_float_int != empty_sorted_dict_float_bool_1 + assert empty_sorted_dict_float_bool_1 == empty_sorted_dict_float_bool_2 + assert sorted_dict_float_int_1 == sorted_dict_float_int_2 + assert sorted_dict_float_int_1 != sorted_dict_float_int_3 + +def test_sub_and_remove_sub(): + """Test SortedDict methods sub() and remove_sub().""" + + sorted_dict = SortedDict(int, str, {k: 'value of %s' % k for k in (2, 5, 1, 9, 4, 5, 20, 0, 6, 17, 8, 3, 7, 0, 4)}) + assert sorted_dict.sub() == list(sorted_dict.values()) + assert sorted_dict.sub(-10, 4) == ['value of 0', 'value of 1', 'value of 2', 'value of 3', 'value of 4'] + assert sorted_dict.sub(15) == ['value of 17', 'value of 20'] + sorted_dict.remove_sub(-10, 4) + assert all(k not in sorted_dict for k in (0, 1, 2, 3, 4)) + sorted_dict.remove_sub(15) + assert all(k not in sorted_dict for k in (17, 20)) + +def test_is_sequence_and_is_dict(): + """Check sorted dict with is_sequence() and is_dict().""" + + assert common.is_dictionary(SortedDict(str, int, {'a': 3, 'b': -1, 'c': 12})) + assert common.is_dictionary(SortedDict(int, float), ) + assert not common.is_sequence(SortedDict(str, str)) diff --git a/diplomacy/utils/tests/test_sorted_set.py b/diplomacy/utils/tests/test_sorted_set.py new file mode 100644 index 0000000..1208cd3 --- /dev/null +++ b/diplomacy/utils/tests/test_sorted_set.py @@ -0,0 +1,168 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Test class SortedSet. """ +from diplomacy.utils import common +from diplomacy.utils.sorted_set import SortedSet +from diplomacy.utils.tests.test_common import assert_equals + +def test_init_bool_and_len(): + """ Test SortedSet initialization, length and conversion to boolean. """ + + sorted_set = SortedSet(int) + assert not sorted_set + sorted_set = SortedSet(int, (2, 4, 99)) + assert sorted_set + assert len(sorted_set) == 3 + +def test_builder_and_property(): + """ Test SortedSet builder and property element_type. """ + + builder_float = SortedSet.builder(float) + sorted_set = builder_float((2.5, 2.7, 2.9)) + assert isinstance(sorted_set, SortedSet) and sorted_set.element_type is float + +def test_item_add_get_and_contains(): + """ Test SortedSet methods add(), __getitem__(), and __contains__(). """ + + expected_values = ['cat', 'lion', 'panthera', 'serval', 'tiger'] + sorted_set = SortedSet(str, ('lion', 'tiger')) + # Test setter. + sorted_set.add('panthera') + sorted_set.add('cat') + sorted_set.add('serval') + # Test __contains__. + assert 'lions' not in sorted_set + assert all(key in sorted_set for key in expected_values) + # Test getter. + assert sorted_set[0] == 'cat' + assert sorted_set[1] == 'lion' + assert sorted_set[2] == 'panthera' + assert sorted_set[3] == 'serval' + assert sorted_set[4] == 'tiger' + # Test add then getter. + sorted_set.add('onca') + assert sorted_set[1] == 'lion' + assert sorted_set[2] == 'onca' + assert sorted_set[3] == 'panthera' + +def test_pop_and_remove(): + """ Test SortedSet methods remove() and pop(). """ + + sorted_set = SortedSet(str, ('lion', 'tiger', 'panthera', 'cat', 'serval')) + assert len(sorted_set) == 5 + assert 'serval' in sorted_set + sorted_set.remove('serval') + assert len(sorted_set) == 4 + assert 'serval' not in sorted_set + assert sorted_set.remove('tiger') == 'tiger' + assert len(sorted_set) == 3 + assert 'tiger' not in sorted_set + assert sorted_set.remove('tiger') is None + assert sorted_set.remove('key not in set') is None + index_of_panthera = sorted_set.index('panthera') + assert index_of_panthera == 2 + assert sorted_set.pop(index_of_panthera) == 'panthera' + assert len(sorted_set) == 2 + assert 'panthera' not in sorted_set + assert 'cat' in sorted_set + assert 'lion' in sorted_set + +def test_iteration(): + """ Test SortedSet iteration. """ + + expected_sorted_values = ['cat', 'lion', 'panthera', 'serval', 'tiger'] + sorted_set = SortedSet(str, ('lion', 'tiger', 'panthera', 'cat', 'serval')) + computed_sorted_values = [key for key in sorted_set] + assert_equals(expected_sorted_values, computed_sorted_values) + +def test_equality(): + """ Test SortedSet equality. """ + + empty_sorted_set_float = SortedSet(float) + empty_sorted_set_int = SortedSet(int) + another_empty_sorted_set_int = SortedSet(int) + sorted_set_float_1 = SortedSet(float, (2.5, 3.3, -5.7)) + sorted_set_float_2 = SortedSet(float, (2.5, 3.3, -5.7)) + sorted_set_float_3 = SortedSet(float, (2.5, 3.3, 5.7)) + assert empty_sorted_set_float != empty_sorted_set_int + assert empty_sorted_set_int == another_empty_sorted_set_int + assert sorted_set_float_1 == sorted_set_float_2 + assert sorted_set_float_1 != sorted_set_float_3 + +def test_getters_around_values(): + """Test SortedSet methods get_next_value() and get_previous_value().""" + + sorted_set = SortedSet(int, (2, 5, 1, 9, 4, 5, 20, 0, 6, 17, 8, 3, 7, 0, 4)) + expected = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 20) + assert sorted_set + assert len(sorted_set) == len(expected) + assert all(expected[i] == sorted_set[i] for i in range(len(expected))) + assert all(e in sorted_set for e in expected) + assert sorted_set.get_next_value(0) == 1 + assert sorted_set.get_next_value(5) == 6 + assert sorted_set.get_next_value(9) == 17 + assert sorted_set.get_next_value(-1) == 0 + assert sorted_set.get_next_value(20) is None + assert sorted_set.get_previous_value(0) is None + assert sorted_set.get_previous_value(17) == 9 + assert sorted_set.get_previous_value(20) == 17 + assert sorted_set.get_previous_value(1) == 0 + assert sorted_set.get_previous_value(6) == 5 + + assert sorted_set.get_next_value(3) == 4 + assert sorted_set.get_next_value(4) == 5 + assert sorted_set.get_next_value(7) == 8 + assert sorted_set.get_next_value(8) == 9 + assert sorted_set.get_previous_value(5) == 4 + assert sorted_set.get_previous_value(4) == 3 + assert sorted_set.get_previous_value(9) == 8 + assert sorted_set.get_previous_value(8) == 7 + sorted_set.remove(8) + assert len(sorted_set) == len(expected) - 1 + assert 8 not in sorted_set + sorted_set.remove(4) + assert len(sorted_set) == len(expected) - 2 + assert 4 not in sorted_set + assert sorted_set.get_next_value(3) == 5 + assert sorted_set.get_next_value(4) == 5 + assert sorted_set.get_next_value(7) == 9 + assert sorted_set.get_next_value(8) == 9 + assert sorted_set.get_previous_value(5) == 3 + assert sorted_set.get_previous_value(4) == 3 + assert sorted_set.get_previous_value(9) == 7 + assert sorted_set.get_previous_value(8) == 7 + +def test_index(): + """ Test SortedSet method index(). """ + + sorted_set = SortedSet(int, (2, 5, 1, 9, 4, 5, 20, 0, 6, 17, 8, 3, 7, 0, 4)) + sorted_set.remove(8) + sorted_set.remove(4) + index_of_2 = sorted_set.index(2) + index_of_17 = sorted_set.index(17) + assert index_of_2 == 2 + assert sorted_set.index(4) is None + assert sorted_set.index(8) is None + assert index_of_17 == len(sorted_set) - 2 + assert sorted_set.pop(index_of_2) == 2 + +def test_common_utils_with_sorted_set(): + """Check sorted set with is_sequence() and is_dictionary().""" + assert common.is_sequence(SortedSet(int, (1, 2, 3))) + assert common.is_sequence(SortedSet(int)) + assert not common.is_dictionary(SortedSet(int, (1, 2, 3))) + assert not common.is_dictionary(SortedSet(int)) diff --git a/diplomacy/utils/tests/test_time.py b/diplomacy/utils/tests/test_time.py new file mode 100644 index 0000000..a2e7a63 --- /dev/null +++ b/diplomacy/utils/tests/test_time.py @@ -0,0 +1,77 @@ +# ============================================================================== +# 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/>. +# ============================================================================== +""" Tests cases for time function""" +from diplomacy.utils import str_to_seconds, next_time_at, trunc_time + +def test_str_to_seconds(): + """ Tests for str_to_seconds """ + assert str_to_seconds('1W') == 604800 + assert str_to_seconds('1D') == 86400 + assert str_to_seconds('1H') == 3600 + assert str_to_seconds('1M') == 60 + assert str_to_seconds('1S') == 1 + assert str_to_seconds('1') == 1 + assert str_to_seconds(1) == 1 + + assert str_to_seconds('10W') == 10 * 604800 + assert str_to_seconds('10D') == 10 * 86400 + assert str_to_seconds('10H') == 10 * 3600 + assert str_to_seconds('10M') == 10 * 60 + assert str_to_seconds('10S') == 10 * 1 + assert str_to_seconds('10') == 10 * 1 + assert str_to_seconds(10) == 10 * 1 + + assert str_to_seconds('1W2D3H4M5S') == 1 * 604800 + 2 * 86400 + 3 * 3600 + 4 * 60 + 5 + assert str_to_seconds('1W2D3H4M5') == 1 * 604800 + 2 * 86400 + 3 * 3600 + 4 * 60 + 5 + assert str_to_seconds('11W12D13H14M15S') == 11 * 604800 + 12 * 86400 + 13 * 3600 + 14 * 60 + 15 + assert str_to_seconds('11W12D13H14M15') == 11 * 604800 + 12 * 86400 + 13 * 3600 + 14 * 60 + 15 + +def test_trunc_time(): + """ Tests for trunc_time """ + # 1498746123 = Thursday, June 29, 2017 10:22:03 AM GMT-04:00 DST + assert trunc_time(1498746123, '1M', 'America/Montreal') == 1498746180 # 10:23 + assert trunc_time(1498746123, '5M', 'America/Montreal') == 1498746300 # 10:25 + assert trunc_time(1498746123, '10M', 'America/Montreal') == 1498746600 # 10:30 + assert trunc_time(1498746123, '15M', 'America/Montreal') == 1498746600 # 10:30 + assert trunc_time(1498746123, '20M', 'America/Montreal') == 1498747200 # 10:40 + assert trunc_time(1498746123, '25M', 'America/Montreal') == 1498746300 # 10:25 + + # 1498731723 = Thursday, June 29, 2017 10:22:03 AM GMT + assert trunc_time(1498731723, '1M', 'GMT') == 1498731780 # 10:23 + assert trunc_time(1498731723, '5M', 'GMT') == 1498731900 # 10:25 + assert trunc_time(1498731723, '10M', 'GMT') == 1498732200 # 10:30 + assert trunc_time(1498731723, '15M', 'GMT') == 1498732200 # 10:30 + assert trunc_time(1498731723, '20M', 'GMT') == 1498732800 # 10:40 + assert trunc_time(1498731723, '25M', 'GMT') == 1498731900 # 10:25 + +def test_next_time_at(): + """ Tests for next_time_at """ + # 1498746123 = Thursday, June 29, 2017 10:22:03 AM GMT-04:00 DST + assert next_time_at(1498746123, '10:23', 'America/Montreal') == 1498746180 # 10:23 + assert next_time_at(1498746123, '10:25', 'America/Montreal') == 1498746300 # 10:25 + assert next_time_at(1498746123, '10:30', 'America/Montreal') == 1498746600 # 10:30 + assert next_time_at(1498746123, '10:40', 'America/Montreal') == 1498747200 # 10:40 + assert next_time_at(1498746123, '16:40', 'America/Montreal') == 1498768800 # 16:40 + assert next_time_at(1498746123, '6:20', 'America/Montreal') == 1498818000 # 6:20 (Next day) + + # 1498731723 = Thursday, June 29, 2017 10:22:03 AM GMT + assert next_time_at(1498731723, '10:23', 'GMT') == 1498731780 # 10:23 + assert next_time_at(1498731723, '10:25', 'GMT') == 1498731900 # 10:25 + assert next_time_at(1498731723, '10:30', 'GMT') == 1498732200 # 10:30 + assert next_time_at(1498731723, '10:40', 'GMT') == 1498732800 # 10:40 + assert next_time_at(1498731723, '16:40', 'GMT') == 1498754400 # 16:40 + assert next_time_at(1498731723, '6:20', 'GMT') == 1498803600 # 6:20 (Next day) diff --git a/diplomacy/utils/time.py b/diplomacy/utils/time.py new file mode 100644 index 0000000..6f250dd --- /dev/null +++ b/diplomacy/utils/time.py @@ -0,0 +1,85 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Time functions + - Contains generic time functions (e.g. to calculate deadlines) +""" +import calendar +import datetime +import math +import pytz + +def str_to_seconds(offset_str): + """ Converts a time in format 00W00D00H00M00S in number of seconds + :param offset_str: The string to convert (e.g. '20D') + :return: Its equivalent in seconds = 1728000 + """ + mult = {'W': 7 * 24 * 60 * 60, 'D': 24 * 60 * 60, 'H': 60 * 60, 'M': 60, 'S': 1, ' ': 1} + buffer = current_sum = 0 + offset_str = str(offset_str) + + # Adding digits to buffer, when a character is found, + # multiply it with buffer and increase the current_sum + for char in offset_str: + if char.isdigit(): + buffer = buffer * 10 + int(char) + elif char.upper() in mult: + current_sum += buffer * mult[char.upper()] + buffer = 0 + else: + buffer = 0 + current_sum += buffer + + return current_sum + +def trunc_time(timestamp, trunc_interval, time_zone='GMT'): + """ Truncates time at a specific interval (e.g. 20M) (i.e. Rounds to the next :20, :40, :60) + :param timestamp: The unix epoch to truncate (e.g. 1498746120) + :param trunc_interval: The truncation interval (e.g. 60*60 or '1H') + :param time_zone: The time to use for conversion (defaults to GMT otherwise) + :return: A timestamp truncated to the nearest (future) interval + """ + midnight_ts = calendar.timegm(datetime.datetime.combine(datetime.date.today(), datetime.time.min).utctimetuple()) + midnight_offset = (timestamp - midnight_ts) % (24*3600) + + dtime = datetime.datetime.fromtimestamp(timestamp, pytz.timezone(time_zone)) + tz_offset = dtime.utcoffset().total_seconds() + interval = str_to_seconds(trunc_interval) + trunc_offset = math.ceil((midnight_offset + tz_offset) / interval) * interval + + trunc_ts = timestamp - midnight_offset + trunc_offset - tz_offset + return int(trunc_ts) + +def next_time_at(timestamp, time_at, time_zone='GMT'): + """ Returns the next timestamp at a specific 'hh:mm' + :param timestamp: The unix timestamp to convert + :param time_at: The next 'hh:mm' to have the time rounded to, or 0 to skip + :param time_zone: The time to use for conversion (defaults to GMT otherwise) + :return: A timestamp truncated to the nearest (future) hh:mm + """ + if not time_at: + return timestamp + + midnight_ts = calendar.timegm(datetime.datetime.combine(datetime.date.today(), datetime.time.min).utctimetuple()) + midnight_offset = (timestamp - midnight_ts) % (24*3600) + + dtime = datetime.datetime.fromtimestamp(timestamp, pytz.timezone(time_zone)) + tz_offset = dtime.utcoffset().total_seconds() + trunc_interval = '%dH%dM' % (int(time_at.split(':')[0]), int(time_at.split(':')[1])) if ':' in time_at else time_at + interval = str_to_seconds(trunc_interval) + at_offset = (-midnight_offset + interval - tz_offset) % (24 * 3600) + at_ts = timestamp + at_offset + return int(at_ts) diff --git a/diplomacy/web/.eslintignore b/diplomacy/web/.eslintignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/diplomacy/web/.eslintignore @@ -0,0 +1 @@ +build diff --git a/diplomacy/web/.eslintrc b/diplomacy/web/.eslintrc new file mode 100644 index 0000000..85a4876 --- /dev/null +++ b/diplomacy/web/.eslintrc @@ -0,0 +1,25 @@ +{ + "plugins": [ + "react" + ], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "env": { + "es6": true, + "browser": true, + "node": true, + "mocha": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "rules": { + "semi": [2, "always", { "omitLastInOneLineBlock": true}] + } +} diff --git a/diplomacy/web/.gitignore b/diplomacy/web/.gitignore new file mode 100644 index 0000000..d30f40e --- /dev/null +++ b/diplomacy/web/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/diplomacy/web/package-lock.json b/diplomacy/web/package-lock.json new file mode 100644 index 0000000..bbb290a --- /dev/null +++ b/diplomacy/web/package-lock.json @@ -0,0 +1,11509 @@ +{ + "name": "web", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@githubprimer/octicons-react": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@githubprimer/octicons-react/-/octicons-react-8.0.0.tgz", + "integrity": "sha512-0dBJ8Pxe94g1RzULybp0zDWiFpZISAIaRY4LP8ZZnweJgyIoLGXy/6bGycJnPHfmk5sbhcuzzNDufLLDA2vxWA==", + "requires": { + "prop-types": "^15.6.1" + } + }, + "abab": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", + "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=" + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.6.2.tgz", + "integrity": "sha512-zUzo1E5dI2Ey8+82egfnttyMlMZ2y0D8xOCO3PNPPlYXpl8NZvF6Qk9L9BEtJs+43FqEmfBViDqc5d1ckRDguw==" + }, + "acorn-dynamic-import": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", + "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", + "requires": { + "acorn": "^4.0.3" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", + "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "requires": { + "acorn": "^4.0.4" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + } + } + }, + "address": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz", + "integrity": "sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg==" + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=" + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "requires": { + "string-width": "^2.0.0" + } + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + } + } + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.7.1.tgz", + "integrity": "sha1-Jsu1r/ZBRLCoJb4YRuCxbPoAsR4=", + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, + "array-flatten": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", + "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=" + }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.7.0" + } + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=" + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=" + }, + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "^4.17.10" + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" + }, + "autoprefixer": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.1.6.tgz", + "integrity": "sha512-C9yv/UF3X+eJTi/zvfxuyfxmLibYrntpF3qoJYrMeQwgUJOZrZvpJiMG2FMQ3qnhWtF/be4pYONBBw95ZGe3vA==", + "requires": { + "browserslist": "^2.5.1", + "caniuse-lite": "^1.0.30000748", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^6.0.13", + "postcss-value-parser": "^3.2.3" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "axobject-query": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz", + "integrity": "sha1-YvWdvFnJ+SQnWco0mWDnov48NsA=", + "requires": { + "ast-types-flow": "0.0.7" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-core": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", + "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.0", + "debug": "^2.6.8", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.7", + "slash": "^1.0.0", + "source-map": "^0.5.6" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "babel-eslint": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", + "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", + "requires": { + "babel-code-frame": "^6.22.0", + "babel-traverse": "^6.23.1", + "babel-types": "^6.23.0", + "babylon": "^6.17.0" + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "requires": { + "babel-helper-explode-assignable-expression": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-builder-react-jsx": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", + "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "esutils": "^2.0.2" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "requires": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "requires": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-jest": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-20.0.3.tgz", + "integrity": "sha1-5KA7E9wQOJ4UD8ZF0J/8TO0wFnE=", + "requires": { + "babel-core": "^6.0.0", + "babel-plugin-istanbul": "^4.0.0", + "babel-preset-jest": "^20.0.3" + } + }, + "babel-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.2.tgz", + "integrity": "sha512-jRwlFbINAeyDStqK6Dd5YuY0k5YuzQUvlz2ZamuXrXmxav3pNqe9vfJ402+2G+OmlJSXxCOpB6Uz0INM7RQe2A==", + "requires": { + "find-cache-dir": "^1.0.0", + "loader-utils": "^1.0.2", + "mkdirp": "^0.5.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-dynamic-import-node": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-1.1.0.tgz", + "integrity": "sha512-tTfZbM9Ecwj3GK50mnPrUpinTwA4xXmDiQGCk/aBYbvl1+X8YqldK86wZ1owVJ4u3mrKbRlXMma80J18qwiaTQ==", + "requires": { + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "babel-plugin-istanbul": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", + "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.13.0", + "find-up": "^2.1.0", + "istanbul-lib-instrument": "^1.10.1", + "test-exclude": "^4.2.1" + } + }, + "babel-plugin-jest-hoist": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz", + "integrity": "sha1-r+3IU70/jcNUjqZx++adA8wsF2c=" + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=" + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=" + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=" + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=" + }, + "babel-plugin-syntax-flow": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", + "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=" + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=" + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=" + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "requires": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-plugin-syntax-class-properties": "^6.8.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "requires": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "requires": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "requires": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "requires": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "requires": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "requires": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", + "babel-plugin-syntax-exponentiation-operator": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-flow-strip-types": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", + "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", + "requires": { + "babel-plugin-syntax-flow": "^6.18.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "babel-plugin-transform-react-constant-elements": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-constant-elements/-/babel-plugin-transform-react-constant-elements-6.23.0.tgz", + "integrity": "sha1-LxGb9NLN1F65uqrldAU8YE9hR90=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-react-display-name": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", + "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-react-jsx": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", + "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", + "requires": { + "babel-helper-builder-react-jsx": "^6.24.1", + "babel-plugin-syntax-jsx": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-react-jsx-self": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", + "integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=", + "requires": { + "babel-plugin-syntax-jsx": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-react-jsx-source": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", + "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", + "requires": { + "babel-plugin-syntax-jsx": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "requires": { + "regenerator-transform": "^0.10.0" + } + }, + "babel-plugin-transform-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-preset-env": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.1.tgz", + "integrity": "sha512-W6VIyA6Ch9ePMI7VptNn2wBM6dbG0eSz25HEiL40nQXCsXGTGZSTZu1Iap+cj3Q0S5a7T9+529l/5Bkvd+afNA==", + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", + "babel-plugin-transform-async-to-generator": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.23.0", + "babel-plugin-transform-es2015-classes": "^6.23.0", + "babel-plugin-transform-es2015-computed-properties": "^6.22.0", + "babel-plugin-transform-es2015-destructuring": "^6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0", + "babel-plugin-transform-es2015-for-of": "^6.23.0", + "babel-plugin-transform-es2015-function-name": "^6.22.0", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.22.0", + "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", + "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0", + "babel-plugin-transform-es2015-modules-umd": "^6.23.0", + "babel-plugin-transform-es2015-object-super": "^6.22.0", + "babel-plugin-transform-es2015-parameters": "^6.23.0", + "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.22.0", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.22.0", + "babel-plugin-transform-exponentiation-operator": "^6.22.0", + "babel-plugin-transform-regenerator": "^6.22.0", + "browserslist": "^2.1.2", + "invariant": "^2.2.2", + "semver": "^5.3.0" + } + }, + "babel-preset-flow": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", + "integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=", + "requires": { + "babel-plugin-transform-flow-strip-types": "^6.22.0" + } + }, + "babel-preset-jest": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-20.0.3.tgz", + "integrity": "sha1-y6yq3stdaJyh4d4TYOv8ZoYsF4o=", + "requires": { + "babel-plugin-jest-hoist": "^20.0.3" + } + }, + "babel-preset-react": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", + "integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=", + "requires": { + "babel-plugin-syntax-jsx": "^6.3.13", + "babel-plugin-transform-react-display-name": "^6.23.0", + "babel-plugin-transform-react-jsx": "^6.24.1", + "babel-plugin-transform-react-jsx-self": "^6.22.0", + "babel-plugin-transform-react-jsx-source": "^6.22.0", + "babel-preset-flow": "^6.23.0" + } + }, + "babel-preset-react-app": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-3.1.1.tgz", + "integrity": "sha512-9fRHopNaGL5ScRZdPSoyxRaABKmkS2fx0HUJ5Yphan5G8QDFD7lETsPyY7El6b7YPT3sNrw9gfrWzl4/LsJcfA==", + "requires": { + "babel-plugin-dynamic-import-node": "1.1.0", + "babel-plugin-syntax-dynamic-import": "6.18.0", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-object-rest-spread": "6.26.0", + "babel-plugin-transform-react-constant-elements": "6.23.0", + "babel-plugin-transform-react-jsx": "6.24.1", + "babel-plugin-transform-react-jsx-self": "6.22.0", + "babel-plugin-transform-react-jsx-source": "6.22.0", + "babel-plugin-transform-regenerator": "6.26.0", + "babel-plugin-transform-runtime": "6.23.0", + "babel-preset-env": "1.6.1", + "babel-preset-react": "6.24.1" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "requires": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + }, + "dependencies": { + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + } + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==" + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "bootstrap": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.3.tgz", + "integrity": "sha512-rDFIzgXcof0jDyjNosjv4Sno77X4KuPeFxG2XZZv1/Kc8DRVGVADdoQyyOVDwPqL36DDmtCQbrpMCqvpPLJQ0w==" + }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-resolve": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", + "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + } + } + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz", + "integrity": "sha512-zy0Cobe3hhgpiOM32Tj7KQ3Vl91m0njwsjzZQK1L+JDf11dzP9qIvjreVinsvXrgfjhStXwUWAEpB9D7Gwmayw==", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "requires": { + "caniuse-lite": "^1.0.30000792", + "electron-to-chromium": "^1.3.30" + } + }, + "bser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", + "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==" + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=" + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + } + } + }, + "caniuse-api": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", + "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", + "requires": { + "browserslist": "^1.3.6", + "caniuse-db": "^1.0.30000529", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + }, + "dependencies": { + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "requires": { + "caniuse-db": "^1.0.30000639", + "electron-to-chromium": "^1.2.7" + } + } + } + }, + "caniuse-db": { + "version": "1.0.30000853", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000853.tgz", + "integrity": "sha1-MpAafWuTqH1Z8Iqu5H6o/27JD98=" + }, + "caniuse-lite": { + "version": "1.0.30000853", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000853.tgz", + "integrity": "sha512-vMrE8BED4MJC9IhDJKP8ok6bJUfn5+YHvxwXMYfiPqQOJ3r2B9ihcArlUnXu6yPWf7b3jHqiEBwXZEbrbiFUqg==" + }, + "capture-stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" + }, + "case-sensitive-paths-webpack-plugin": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.1.tgz", + "integrity": "sha1-PSnO2MHxJL9vU4Rvs/WJRzH9yQk=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" + }, + "chokidar": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", + "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "fsevents": "^1.1.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "requires": { + "is-extglob": "^2.1.1" + } + } + } + }, + "ci-info": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", + "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==" + }, + "clap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", + "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "requires": { + "chalk": "^1.1.3" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "requires": { + "source-map": "0.5.x" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "coa": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "requires": { + "q": "^1.1.2" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "requires": { + "clone": "^1.0.2", + "color-convert": "^1.3.0", + "color-string": "^0.3.0" + } + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" + }, + "color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "requires": { + "color-name": "^1.0.0" + } + }, + "colormin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz", + "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=", + "requires": { + "color": "^0.11.0", + "css-color-names": "0.0.4", + "has": "^1.0.1" + } + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + }, + "compare-versions": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz", + "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "compressible": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz", + "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=", + "requires": { + "mime-db": ">= 1.34.0 < 2" + }, + "dependencies": { + "mime-db": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.34.0.tgz", + "integrity": "sha1-RS0Oz/XDA0am3B5kseruDTcZ/5o=" + } + } + }, + "compression": { + "version": "1.7.2", + "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", + "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", + "requires": { + "accepts": "~1.3.4", + "bytes": "3.0.0", + "compressible": "~2.0.13", + "debug": "2.6.9", + "on-headers": "~1.0.1", + "safe-buffer": "5.1.1", + "vary": "~1.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=" + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "requires": { + "date-now": "^0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "content-type-parser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.2.tgz", + "integrity": "sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ==" + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz", + "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==", + "requires": { + "is-directory": "^0.3.1", + "js-yaml": "^3.4.3", + "minimist": "^1.2.0", + "object-assign": "^4.1.0", + "os-homedir": "^1.0.1", + "parse-json": "^2.2.0", + "require-from-string": "^1.1.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=" + }, + "css-loader": { + "version": "0.28.7", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz", + "integrity": "sha512-GxMpax8a/VgcfRrVy0gXD6yLd5ePYbXX/5zGgTVYp4wXtJklS8Z2VaUArJgc//f6/Dzil7BaJObdSv8eKKCPgg==", + "requires": { + "babel-code-frame": "^6.11.0", + "css-selector-tokenizer": "^0.7.0", + "cssnano": ">=2.6.1 <4", + "icss-utils": "^2.1.0", + "loader-utils": "^1.0.2", + "lodash.camelcase": "^4.3.0", + "object-assign": "^4.0.1", + "postcss": "^5.0.6", + "postcss-modules-extract-imports": "^1.0.0", + "postcss-modules-local-by-default": "^1.0.1", + "postcss-modules-scope": "^1.0.0", + "postcss-modules-values": "^1.1.0", + "postcss-value-parser": "^3.3.0", + "source-list-map": "^2.0.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-selector-tokenizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", + "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "requires": { + "cssesc": "^0.1.0", + "fastparse": "^1.1.1", + "regexpu-core": "^1.0.0" + }, + "dependencies": { + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + } + } + }, + "css-what": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=" + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=" + }, + "cssnano": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", + "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=", + "requires": { + "autoprefixer": "^6.3.1", + "decamelize": "^1.1.2", + "defined": "^1.0.0", + "has": "^1.0.1", + "object-assign": "^4.0.1", + "postcss": "^5.0.14", + "postcss-calc": "^5.2.0", + "postcss-colormin": "^2.1.8", + "postcss-convert-values": "^2.3.4", + "postcss-discard-comments": "^2.0.4", + "postcss-discard-duplicates": "^2.0.1", + "postcss-discard-empty": "^2.0.1", + "postcss-discard-overridden": "^0.1.1", + "postcss-discard-unused": "^2.2.1", + "postcss-filter-plugins": "^2.0.0", + "postcss-merge-idents": "^2.1.5", + "postcss-merge-longhand": "^2.0.1", + "postcss-merge-rules": "^2.0.3", + "postcss-minify-font-values": "^1.0.2", + "postcss-minify-gradients": "^1.0.1", + "postcss-minify-params": "^1.0.4", + "postcss-minify-selectors": "^2.0.4", + "postcss-normalize-charset": "^1.1.0", + "postcss-normalize-url": "^3.0.7", + "postcss-ordered-values": "^2.1.0", + "postcss-reduce-idents": "^2.2.2", + "postcss-reduce-initial": "^1.0.0", + "postcss-reduce-transforms": "^1.0.3", + "postcss-svgo": "^2.1.1", + "postcss-unique-selectors": "^2.0.2", + "postcss-value-parser": "^3.2.3", + "postcss-zindex": "^2.0.1" + }, + "dependencies": { + "autoprefixer": { + "version": "6.7.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", + "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", + "requires": { + "browserslist": "^1.7.6", + "caniuse-db": "^1.0.30000634", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^5.2.16", + "postcss-value-parser": "^3.2.3" + } + }, + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "requires": { + "caniuse-db": "^1.0.30000639", + "electron-to-chromium": "^1.2.7" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "csso": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", + "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", + "requires": { + "clap": "^1.0.9", + "source-map": "^0.5.3" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "cssom": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", + "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=" + }, + "cssstyle": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", + "requires": { + "cssom": "0.3.x" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "^1.0.1" + } + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "^0.10.9" + } + }, + "damerau-levenshtein": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", + "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "requires": { + "strip-bom": "^3.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + } + } + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "requires": { + "foreach": "^2.0.5", + "object-keys": "^1.0.8" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "requires": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "requires": { + "repeating": "^2.0.0" + } + }, + "detect-node": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", + "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=" + }, + "detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "requires": { + "address": "^1.0.1", + "debug": "^2.6.0" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=" + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-converter": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", + "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", + "requires": { + "utila": "~0.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=" + } + } + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "requires": { + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" + } + } + }, + "dom-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/dom-urls/-/dom-urls-1.1.0.tgz", + "integrity": "sha1-AB3fgWKM0ecGElxxdvU8zsVdkY4=", + "requires": { + "urijs": "^1.16.1" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" + }, + "domhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", + "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "requires": { + "is-obj": "^1.0.0" + } + }, + "dotenv": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz", + "integrity": "sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=" + }, + "dotenv-expand": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-4.2.0.tgz", + "integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=" + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "electron-to-chromium": { + "version": "1.3.48", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz", + "integrity": "sha1-07DYWTgUBE4JLs4hCPw6ya6kuQA=" + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emoji-regex": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", + "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==" + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, + "enhanced-resolve": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", + "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "object-assign": "^4.0.1", + "tapable": "^0.2.7" + } + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "requires": { + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "requires": { + "is-callable": "^1.1.1", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.1" + } + }, + "es5-ext": { + "version": "0.10.45", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz", + "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "requires": { + "d": "1", + "es5-ext": "^0.10.14", + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.10.0.tgz", + "integrity": "sha512-fjUOf8johsv23WuIKdNQU4P9t9jhQ4Qzx6pC2uW890OloK3Zs1ZAoCNpg/2larNF501jLl3UNy0kIRcF6VI22g==", + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + } + } + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "requires": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", + "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", + "dev": true, + "requires": { + "ajv": "^5.3.0", + "babel-code-frame": "^6.22.0", + "chalk": "^2.1.0", + "concat-stream": "^1.6.0", + "cross-spawn": "^5.1.0", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^3.7.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^3.5.4", + "esquery": "^1.0.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.0.1", + "ignore": "^3.3.3", + "imurmurhash": "^0.1.4", + "inquirer": "^3.0.6", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.9.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^1.0.1", + "require-uncached": "^1.0.3", + "semver": "^5.3.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "~2.0.1", + "table": "4.0.2", + "text-table": "~0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "globals": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.5.0.tgz", + "integrity": "sha512-hYyf+kI8dm3nORsiiXUQigOU62hDLfJ9G01uyGMxhc6BKsircrUhC4uJPQPUSuq2GrTmiiEt7ewxlMdBewfmKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "eslint-config-react-app": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-2.1.0.tgz", + "integrity": "sha512-8QZrKWuHVC57Fmu+SsKAVxnI9LycZl7NFQ4H9L+oeISuCXhYdXqsOOIVSjQFW6JF5MXZLFE+21Syhd7mF1IRZQ==" + }, + "eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "requires": { + "debug": "^2.6.9", + "resolve": "^1.5.0" + } + }, + "eslint-loader": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-1.9.0.tgz", + "integrity": "sha512-40aN976qSNPyb9ejTqjEthZITpls1SVKtwguahmH1dzGCwQU/vySE+xX33VZmD8csU0ahVNCtFlsPgKqRBiqgg==", + "requires": { + "loader-fs-cache": "^1.0.0", + "loader-utils": "^1.0.2", + "object-assign": "^4.0.1", + "object-hash": "^1.1.4", + "rimraf": "^2.6.1" + } + }, + "eslint-module-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz", + "integrity": "sha1-snA2LNiLGkitMIl2zn+lTphBF0Y=", + "requires": { + "debug": "^2.6.8", + "pkg-dir": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "requires": { + "find-up": "^1.0.0" + } + } + } + }, + "eslint-plugin-flowtype": { + "version": "2.39.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.39.1.tgz", + "integrity": "sha512-RiQv+7Z9QDJuzt+NO8sYgkLGT+h+WeCrxP7y8lI7wpU41x3x/2o3PGtHk9ck8QnA9/mlbNcy/hG0eKvmd7npaA==", + "requires": { + "lodash": "^4.15.0" + } + }, + "eslint-plugin-import": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz", + "integrity": "sha512-Rf7dfKJxZ16QuTgVv1OYNxkZcsu/hULFnC+e+w0Gzi6jMC3guQoWQgxYxc54IDRinlb6/0v5z/PxxIKmVctN+g==", + "requires": { + "builtin-modules": "^1.1.1", + "contains-path": "^0.1.0", + "debug": "^2.6.8", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.1", + "eslint-module-utils": "^2.1.1", + "has": "^1.0.1", + "lodash.cond": "^4.3.0", + "minimatch": "^3.0.3", + "read-pkg-up": "^2.0.0" + }, + "dependencies": { + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "requires": { + "pify": "^2.0.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-5.1.1.tgz", + "integrity": "sha512-5I9SpoP7gT4wBFOtXT8/tXNPYohHBVfyVfO17vkbC7r9kEIxYJF12D3pKqhk8+xnk12rfxKClS3WCFpVckFTPQ==", + "requires": { + "aria-query": "^0.7.0", + "array-includes": "^3.0.3", + "ast-types-flow": "0.0.7", + "axobject-query": "^0.1.0", + "damerau-levenshtein": "^1.0.0", + "emoji-regex": "^6.1.0", + "jsx-ast-utils": "^1.4.0" + } + }, + "eslint-plugin-react": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.10.0.tgz", + "integrity": "sha512-18rzWn4AtbSUxFKKM7aCVcj5LXOhOKdwBino3KKWy4psxfPW0YtIbE8WNRDUdyHFL50BeLb6qFd4vpvNYyp7hw==", + "dev": true, + "requires": { + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.0.1", + "prop-types": "^15.6.2" + }, + "dependencies": { + "jsx-ast-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz", + "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", + "dev": true, + "requires": { + "array-includes": "^3.0.3" + } + } + } + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "eventemitter3": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "eventsource": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz", + "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=", + "requires": { + "original": ">=0.0.5" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "exec-sh": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", + "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", + "requires": { + "merge": "^1.1.3" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "^2.1.0" + }, + "dependencies": { + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "express": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.3", + "qs": "6.5.1", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extract-text-webpack-plugin": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz", + "integrity": "sha512-bt/LZ4m5Rqt/Crl2HiKuAl/oqg0psx1tsTLkvWbJen1CtD+fftkZhMaQ9HOtY2gWsl2Wq+sABmMVi9z3DhKWQQ==", + "requires": { + "async": "^2.4.1", + "loader-utils": "^1.1.0", + "schema-utils": "^0.3.0", + "webpack-sources": "^1.0.1" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fancybox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fancybox/-/fancybox-3.0.1.tgz", + "integrity": "sha512-HZ57O6j5Zk4Uvusd5pNUgWi7CCjKx+HFlCzjMPeRJ//0bQWUK+xVpkn3EzXWJate4ZSKNFP9kU2l49MP1g+oow==" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=" + }, + "faye-websocket": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", + "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fb-watchman": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "requires": { + "bser": "^2.0.0" + } + }, + "fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "file-loader": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.5.tgz", + "integrity": "sha512-RzGHDatcVNpGISTvCpfUfOGpYuSR7HSsSg87ki+wF6rw1Hm0RALPTiAdsxAq1UwLf0RRhbe22/eHK6nhXspiOQ==", + "requires": { + "loader-utils": "^1.0.2", + "schema-utils": "^0.3.0" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "requires": { + "glob": "^7.0.3", + "minimatch": "^3.0.3" + } + }, + "filesize": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.5.11.tgz", + "integrity": "sha512-ZH7loueKBoDb7yG9esn1U+fgq7BzlzW6NRi5/rMdxIZ05dj7GFD/Xc5rq2CDt5Yq86CyfSYVyx4242QQNZbx1g==" + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + } + }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "requires": { + "circular-json": "^0.3.1", + "del": "^2.0.2", + "graceful-fs": "^4.1.2", + "write": "^0.2.1" + } + }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=" + }, + "follow-redirects": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.0.tgz", + "integrity": "sha512-fdrt472/9qQ6Kgjvb935ig6vJCuofpBUD14f9Vb+SLlm7xIe4Qva5gey8EKtv8lp7ahE1wilg3xL1znpVGtZIA==", + "requires": { + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "^1.0.1" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": "^2.1.0" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "optional": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "^2.0.0" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "requires": { + "ini": "^1.3.4" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" + }, + "gzip-size": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", + "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", + "requires": { + "duplexer": "^0.1.1" + } + }, + "handle-thing": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", + "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=" + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": ">=0.0.4" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "optional": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "optional": true + } + } + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "optional": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.4.tgz", + "integrity": "sha512-A6RlQvvZEtFS5fLU43IDu0QUmBy+fDO9VMdTXvufKwIkt/rFfvICAViCax5fbDO4zdNzaC3/27ZhKUok5bAJyw==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.0" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + } + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==" + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "html-comment-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", + "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=" + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" + }, + "html-minifier": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.16.tgz", + "integrity": "sha512-zP5EfLSpiLRp0aAgud4CQXPQZm9kXwWjR/cF0PfdOj+jjWnOaCgeZcll4kYXSvIBPeUMmyaSc7mM4IDtA+kboA==", + "requires": { + "camel-case": "3.0.x", + "clean-css": "4.1.x", + "commander": "2.15.x", + "he": "1.1.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.3.x" + } + }, + "html-webpack-plugin": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-2.29.0.tgz", + "integrity": "sha1-6Yf0IYU9O2k4yMTIFxhC5f0XryM=", + "requires": { + "bluebird": "^3.4.7", + "html-minifier": "^3.2.3", + "loader-utils": "^0.2.16", + "lodash": "^4.17.3", + "pretty-error": "^2.0.2", + "toposort": "^1.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + } + } + }, + "htmlparser2": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", + "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", + "requires": { + "domelementtype": "1", + "domhandler": "2.1", + "domutils": "1.1", + "readable-stream": "1.0" + }, + "dependencies": { + "domutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", + "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", + "requires": { + "domelementtype": "1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "http-parser-js": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", + "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=" + }, + "http-proxy": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", + "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "requires": { + "eventemitter3": "^3.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz", + "integrity": "sha1-ZC6ISIUdZvCdTxJJEoRtuutBuDM=", + "requires": { + "http-proxy": "^1.16.2", + "is-glob": "^3.1.0", + "lodash": "^4.17.2", + "micromatch": "^2.3.11" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "^1.0.0" + } + } + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "httpplease": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/httpplease/-/httpplease-0.16.4.tgz", + "integrity": "sha1-04Lr4jDvUHkIC06f/r8xap51wNo=", + "requires": { + "urllite": "~0.5.0", + "xmlhttprequest": "*", + "xtend": "~3.0.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=" + }, + "icss-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", + "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", + "requires": { + "postcss": "^6.0.1" + } + }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" + }, + "ignore": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz", + "integrity": "sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg==" + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + }, + "import-local": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-0.1.1.tgz", + "integrity": "sha1-sReVcqrNwRxqkQCftDDbyrX2aKg=", + "requires": { + "pkg-dir": "^2.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "^2.0.0" + } + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.4", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx-lite": "^4.0.8", + "rx-lite-aggregates": "^4.0.8", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "internal-ip": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-1.2.0.tgz", + "integrity": "sha1-rp+/k7mEh4eF1QqN4bNWlWBYz1w=", + "requires": { + "meow": "^3.3.0" + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "ipaddr.js": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" + }, + "is-ci": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", + "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "requires": { + "ci-info": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + } + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=" + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" + }, + "is-root": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-1.0.0.tgz", + "integrity": "sha1-B7bCM7w5TNnQK6FclmvWZg1jQtU=" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-svg": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz", + "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=", + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul-api": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", + "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", + "requires": { + "async": "^2.1.4", + "compare-versions": "^3.1.0", + "fileset": "^2.0.2", + "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-hook": "^1.2.0", + "istanbul-lib-instrument": "^1.10.1", + "istanbul-lib-report": "^1.1.4", + "istanbul-lib-source-maps": "^1.2.4", + "istanbul-reports": "^1.3.0", + "js-yaml": "^3.7.0", + "mkdirp": "^0.5.1", + "once": "^1.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", + "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==", + "requires": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.2.0", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", + "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==" + }, + "istanbul-lib-hook": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz", + "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==", + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", + "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.0", + "semver": "^5.3.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", + "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", + "requires": { + "istanbul-lib-coverage": "^1.2.0", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.3.tgz", + "integrity": "sha512-fDa0hwU/5sDXwAklXgAoCJCOsFsBplVQ6WBldz5UwaqOzmDhUK4nfuR7/G//G2lERlblUNJB8P6e8cXq3a7MlA==", + "requires": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.1.2", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "istanbul-reports": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", + "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", + "requires": { + "handlebars": "^4.0.3" + } + }, + "jest": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest/-/jest-20.0.4.tgz", + "integrity": "sha1-PdJgwpidba1nix6cxNkZRPbWAqw=", + "requires": { + "jest-cli": "^20.0.4" + }, + "dependencies": { + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "jest-cli": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-20.0.4.tgz", + "integrity": "sha1-5TKxnYiuW8bEF+iwWTpv6VSx3JM=", + "requires": { + "ansi-escapes": "^1.4.0", + "callsites": "^2.0.0", + "chalk": "^1.1.3", + "graceful-fs": "^4.1.11", + "is-ci": "^1.0.10", + "istanbul-api": "^1.1.1", + "istanbul-lib-coverage": "^1.0.1", + "istanbul-lib-instrument": "^1.4.2", + "istanbul-lib-source-maps": "^1.1.0", + "jest-changed-files": "^20.0.3", + "jest-config": "^20.0.4", + "jest-docblock": "^20.0.3", + "jest-environment-jsdom": "^20.0.3", + "jest-haste-map": "^20.0.4", + "jest-jasmine2": "^20.0.4", + "jest-message-util": "^20.0.3", + "jest-regex-util": "^20.0.3", + "jest-resolve-dependencies": "^20.0.3", + "jest-runtime": "^20.0.4", + "jest-snapshot": "^20.0.3", + "jest-util": "^20.0.3", + "micromatch": "^2.3.11", + "node-notifier": "^5.0.2", + "pify": "^2.3.0", + "slash": "^1.0.0", + "string-length": "^1.0.1", + "throat": "^3.0.0", + "which": "^1.2.12", + "worker-farm": "^1.3.1", + "yargs": "^7.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + } + } + }, + "jest-changed-files": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-20.0.3.tgz", + "integrity": "sha1-k5TVzGXEOEBhSb7xv01Sto4D4/g=" + }, + "jest-config": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-20.0.4.tgz", + "integrity": "sha1-43kwqyIXyRNgXv8T5712PsSPruo=", + "requires": { + "chalk": "^1.1.3", + "glob": "^7.1.1", + "jest-environment-jsdom": "^20.0.3", + "jest-environment-node": "^20.0.3", + "jest-jasmine2": "^20.0.4", + "jest-matcher-utils": "^20.0.3", + "jest-regex-util": "^20.0.3", + "jest-resolve": "^20.0.4", + "jest-validate": "^20.0.3", + "pretty-format": "^20.0.3" + } + }, + "jest-diff": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-20.0.3.tgz", + "integrity": "sha1-gfKI/Z5nXw+yPHXxwrGURf5YZhc=", + "requires": { + "chalk": "^1.1.3", + "diff": "^3.2.0", + "jest-matcher-utils": "^20.0.3", + "pretty-format": "^20.0.3" + } + }, + "jest-docblock": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-20.0.3.tgz", + "integrity": "sha1-F76phDQswz2DxQ++FUXqDvqkRxI=" + }, + "jest-environment-jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-20.0.3.tgz", + "integrity": "sha1-BIqKwS7iJfcZBBdxODS7mZeH3pk=", + "requires": { + "jest-mock": "^20.0.3", + "jest-util": "^20.0.3", + "jsdom": "^9.12.0" + } + }, + "jest-environment-node": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-20.0.3.tgz", + "integrity": "sha1-1Ii8RhKvLCRumG6K52caCZFj1AM=", + "requires": { + "jest-mock": "^20.0.3", + "jest-util": "^20.0.3" + } + }, + "jest-haste-map": { + "version": "20.0.5", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-20.0.5.tgz", + "integrity": "sha512-0IKAQjUvuZjMCNi/0VNQQF74/H9KB67hsHJqGiwTWQC6XO5Azs7kLWm+6Q/dwuhvDUvABDOBMFK2/FwZ3sZ07Q==", + "requires": { + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.1.11", + "jest-docblock": "^20.0.3", + "micromatch": "^2.3.11", + "sane": "~1.6.0", + "worker-farm": "^1.3.1" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + } + } + }, + "jest-jasmine2": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-20.0.4.tgz", + "integrity": "sha1-/MWxQReA2RHQQpAu8YWehS5g1eE=", + "requires": { + "chalk": "^1.1.3", + "graceful-fs": "^4.1.11", + "jest-diff": "^20.0.3", + "jest-matcher-utils": "^20.0.3", + "jest-matchers": "^20.0.3", + "jest-message-util": "^20.0.3", + "jest-snapshot": "^20.0.3", + "once": "^1.4.0", + "p-map": "^1.1.1" + } + }, + "jest-matcher-utils": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-20.0.3.tgz", + "integrity": "sha1-s6a443yld4A7CDKpixZPRLeBVhI=", + "requires": { + "chalk": "^1.1.3", + "pretty-format": "^20.0.3" + } + }, + "jest-matchers": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-matchers/-/jest-matchers-20.0.3.tgz", + "integrity": "sha1-ymnbHDLbWm9wf6XgQBq7VXAN/WA=", + "requires": { + "jest-diff": "^20.0.3", + "jest-matcher-utils": "^20.0.3", + "jest-message-util": "^20.0.3", + "jest-regex-util": "^20.0.3" + } + }, + "jest-message-util": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-20.0.3.tgz", + "integrity": "sha1-auwoRDBvyw5udNV5bBAG2W/dgxw=", + "requires": { + "chalk": "^1.1.3", + "micromatch": "^2.3.11", + "slash": "^1.0.0" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + } + } + }, + "jest-mock": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-20.0.3.tgz", + "integrity": "sha1-i8Bw6QQUqhVcEajWTIaaDVxx2lk=" + }, + "jest-regex-util": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-20.0.3.tgz", + "integrity": "sha1-hburXRM+RGJbGfr4xqpRItCF12I=" + }, + "jest-resolve": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-20.0.4.tgz", + "integrity": "sha1-lEiz6La6/BVHlETGSZBFt//ll6U=", + "requires": { + "browser-resolve": "^1.11.2", + "is-builtin-module": "^1.0.0", + "resolve": "^1.3.2" + } + }, + "jest-resolve-dependencies": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-20.0.3.tgz", + "integrity": "sha1-bhSntxevDyyzZnxUneQK8Bexcjo=", + "requires": { + "jest-regex-util": "^20.0.3" + } + }, + "jest-runtime": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-20.0.4.tgz", + "integrity": "sha1-osgCIZxCA/dU3xQE5JAYYWnRJNg=", + "requires": { + "babel-core": "^6.0.0", + "babel-jest": "^20.0.3", + "babel-plugin-istanbul": "^4.0.0", + "chalk": "^1.1.3", + "convert-source-map": "^1.4.0", + "graceful-fs": "^4.1.11", + "jest-config": "^20.0.4", + "jest-haste-map": "^20.0.4", + "jest-regex-util": "^20.0.3", + "jest-resolve": "^20.0.4", + "jest-util": "^20.0.3", + "json-stable-stringify": "^1.0.1", + "micromatch": "^2.3.11", + "strip-bom": "3.0.0", + "yargs": "^7.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + } + } + }, + "jest-snapshot": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-20.0.3.tgz", + "integrity": "sha1-W4R+GtsaTZCFKn+fElCG4YfHZWY=", + "requires": { + "chalk": "^1.1.3", + "jest-diff": "^20.0.3", + "jest-matcher-utils": "^20.0.3", + "jest-util": "^20.0.3", + "natural-compare": "^1.4.0", + "pretty-format": "^20.0.3" + } + }, + "jest-util": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-20.0.3.tgz", + "integrity": "sha1-DAf32A2C9OWmfG+LnD/n9lz9Mq0=", + "requires": { + "chalk": "^1.1.3", + "graceful-fs": "^4.1.11", + "jest-message-util": "^20.0.3", + "jest-mock": "^20.0.3", + "jest-validate": "^20.0.3", + "leven": "^2.1.0", + "mkdirp": "^0.5.1" + } + }, + "jest-validate": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-20.0.3.tgz", + "integrity": "sha1-0M/R3k9XnymEhJJcKA+PHZTsPKs=", + "requires": { + "chalk": "^1.1.3", + "jest-matcher-utils": "^20.0.3", + "leven": "^2.1.0", + "pretty-format": "^20.0.3" + } + }, + "jquery": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", + "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" + }, + "js-base64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz", + "integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ==" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "js-yaml": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", + "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "requires": { + "argparse": "^1.0.7", + "esprima": "^2.6.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jsdom": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-9.12.0.tgz", + "integrity": "sha1-6MVG//ywbADUgzyoRBD+1/igl9Q=", + "requires": { + "abab": "^1.0.3", + "acorn": "^4.0.4", + "acorn-globals": "^3.1.0", + "array-equal": "^1.0.0", + "content-type-parser": "^1.0.1", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": ">= 0.2.37 < 0.3.0", + "escodegen": "^1.6.1", + "html-encoding-sniffer": "^1.0.1", + "nwmatcher": ">= 1.3.9 < 2.0.0", + "parse5": "^1.5.1", + "request": "^2.79.0", + "sax": "^1.2.1", + "symbol-tree": "^3.2.1", + "tough-cookie": "^2.3.2", + "webidl-conversions": "^4.0.0", + "whatwg-encoding": "^1.0.1", + "whatwg-url": "^4.3.0", + "xml-name-validator": "^2.0.1" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" + }, + "json-loader": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", + "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=" + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jsx-ast-utils": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", + "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=" + }, + "killable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", + "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "requires": { + "package-json": "^4.0.0" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "loader-fs-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz", + "integrity": "sha1-VuC/CL2XCLJqdltoUJhAyN7J/bw=", + "requires": { + "find-cache-dir": "^0.1.1", + "mkdirp": "0.5.1" + }, + "dependencies": { + "find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "requires": { + "commondir": "^1.0.1", + "mkdirp": "^0.5.1", + "pkg-dir": "^1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "requires": { + "find-up": "^1.0.0" + } + } + } + }, + "loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=" + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "lodash.cond": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", + "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=" + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + }, + "lodash.template": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", + "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "requires": { + "lodash._reinterpolate": "~3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", + "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "requires": { + "lodash._reinterpolate": "~3.0.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "loglevel": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz", + "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=" + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "^3.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "requires": { + "tmpl": "1.0.x" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "math-expression-evaluator": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", + "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=" + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "merge": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", + "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "~1.33.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-odd": "^2.0.0", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "neo-async": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.1.tgz", + "integrity": "sha512-3KL3fvuRkZ7s4IFOMfztb7zJp3QaVWnBeGoJlgB38XnCRPj/0tLzzLG5IB8NYOHbJ8g8UGrgZv44GLDk6CxTxA==" + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "requires": { + "lower-case": "^1.1.1" + } + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, + "node-forge": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", + "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==" + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + }, + "node-libs-browser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", + "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^1.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.0", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "node-notifier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", + "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", + "requires": { + "growly": "^1.3.0", + "semver": "^5.4.1", + "shellwords": "^0.1.1", + "which": "^1.3.0" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "nwmatcher": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz", + "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-hash": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.0.tgz", + "integrity": "sha512-05KzQ70lSeGSrZJQXE5wNDiTkBJDlUT/myi6RX9dVIvz7a7Qh4oH93BQdiPMn27nldYvVQCKMUaM83AfizZlsQ==" + }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "opn": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.2.0.tgz", + "integrity": "sha512-Jd/GpzPyHF4P2/aNOVmS3lfMSWV9J7cOhCG1s08XCEAsPkB7lp6ddiU0J7XzyQRDUh8BqJ7PchfINjR8jyofRQ==", + "requires": { + "is-wsl": "^1.1.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "original": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.1.tgz", + "integrity": "sha512-IEvtB5vM5ULvwnqMxWBLxkS13JIEXbakizMSo3yoPNPCIWzg8TG3Usn/UhXoZFM/m+FuEA20KdzPSFq/0rS+UA==", + "requires": { + "url-parse": "~1.4.0" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==" + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "requires": { + "no-case": "^2.2.0" + } + }, + "parse-asn1": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, + "parse5": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", + "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=" + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pbkdf2": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", + "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "requires": { + "find-up": "^2.1.0" + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==" + }, + "popper.js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.4.tgz", + "integrity": "sha1-juwdj/AqWjoVLdQ0FKFce3n9abY=" + }, + "portfinder": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz", + "integrity": "sha1-uzLs2HwnEErm7kS1o8y/Drsa7ek=", + "requires": { + "async": "^1.5.2", + "debug": "^2.2.0", + "mkdirp": "0.5.x" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "postcss": { + "version": "6.0.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz", + "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "postcss-calc": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", + "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", + "requires": { + "postcss": "^5.0.2", + "postcss-message-helpers": "^2.0.0", + "reduce-css-calc": "^1.2.6" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-colormin": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz", + "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=", + "requires": { + "colormin": "^1.0.5", + "postcss": "^5.0.13", + "postcss-value-parser": "^3.2.3" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-convert-values": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz", + "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=", + "requires": { + "postcss": "^5.0.11", + "postcss-value-parser": "^3.1.2" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-discard-comments": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz", + "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=", + "requires": { + "postcss": "^5.0.14" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-discard-duplicates": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz", + "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=", + "requires": { + "postcss": "^5.0.4" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-discard-empty": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz", + "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=", + "requires": { + "postcss": "^5.0.14" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-discard-overridden": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz", + "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=", + "requires": { + "postcss": "^5.0.16" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-discard-unused": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz", + "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=", + "requires": { + "postcss": "^5.0.14", + "uniqs": "^2.0.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-filter-plugins": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.3.tgz", + "integrity": "sha512-T53GVFsdinJhgwm7rg1BzbeBRomOg9y5MBVhGcsV0CxurUdVj1UlPdKtn7aqYA/c/QVkzKMjq2bSV5dKG5+AwQ==", + "requires": { + "postcss": "^5.0.4" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-flexbugs-fixes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-3.2.0.tgz", + "integrity": "sha512-0AuD9HG1Ey3/3nqPWu9yqf7rL0KCPu5VgjDsjf5mzEcuo9H/z8nco/fljKgjsOUrZypa95MI0kS4xBZeBzz2lw==", + "requires": { + "postcss": "^6.0.1" + } + }, + "postcss-load-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", + "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", + "requires": { + "cosmiconfig": "^2.1.0", + "object-assign": "^4.1.0", + "postcss-load-options": "^1.2.0", + "postcss-load-plugins": "^2.3.0" + } + }, + "postcss-load-options": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", + "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", + "requires": { + "cosmiconfig": "^2.1.0", + "object-assign": "^4.1.0" + } + }, + "postcss-load-plugins": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", + "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "requires": { + "cosmiconfig": "^2.1.1", + "object-assign": "^4.1.0" + } + }, + "postcss-loader": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.0.8.tgz", + "integrity": "sha512-KtXBiQ/r/WYW8LxTSJK7h8wLqvCMSub/BqmRnud/Mu8RzwflW9cmXxwsMwbn15TNv287Hcufdb3ZSs7xHKnG8Q==", + "requires": { + "loader-utils": "^1.1.0", + "postcss": "^6.0.0", + "postcss-load-config": "^1.2.0", + "schema-utils": "^0.3.0" + } + }, + "postcss-merge-idents": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz", + "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=", + "requires": { + "has": "^1.0.1", + "postcss": "^5.0.10", + "postcss-value-parser": "^3.1.1" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-merge-longhand": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz", + "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=", + "requires": { + "postcss": "^5.0.4" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-merge-rules": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz", + "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=", + "requires": { + "browserslist": "^1.5.2", + "caniuse-api": "^1.5.2", + "postcss": "^5.0.4", + "postcss-selector-parser": "^2.2.2", + "vendors": "^1.0.0" + }, + "dependencies": { + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "requires": { + "caniuse-db": "^1.0.30000639", + "electron-to-chromium": "^1.2.7" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-message-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", + "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=" + }, + "postcss-minify-font-values": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz", + "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=", + "requires": { + "object-assign": "^4.0.1", + "postcss": "^5.0.4", + "postcss-value-parser": "^3.0.2" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-minify-gradients": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz", + "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=", + "requires": { + "postcss": "^5.0.12", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-minify-params": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz", + "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=", + "requires": { + "alphanum-sort": "^1.0.1", + "postcss": "^5.0.2", + "postcss-value-parser": "^3.0.2", + "uniqs": "^2.0.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-minify-selectors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz", + "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=", + "requires": { + "alphanum-sort": "^1.0.2", + "has": "^1.0.1", + "postcss": "^5.0.14", + "postcss-selector-parser": "^2.0.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "requires": { + "postcss": "^6.0.1" + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "requires": { + "icss-replace-symbols": "^1.1.0", + "postcss": "^6.0.1" + } + }, + "postcss-normalize-charset": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz", + "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=", + "requires": { + "postcss": "^5.0.5" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-normalize-url": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz", + "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=", + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^1.4.0", + "postcss": "^5.0.14", + "postcss-value-parser": "^3.2.3" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-ordered-values": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz", + "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=", + "requires": { + "postcss": "^5.0.4", + "postcss-value-parser": "^3.0.1" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-reduce-idents": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz", + "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=", + "requires": { + "postcss": "^5.0.4", + "postcss-value-parser": "^3.0.2" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-reduce-initial": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz", + "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=", + "requires": { + "postcss": "^5.0.4" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-reduce-transforms": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz", + "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=", + "requires": { + "has": "^1.0.1", + "postcss": "^5.0.8", + "postcss-value-parser": "^3.0.1" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-svgo": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz", + "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=", + "requires": { + "is-svg": "^2.0.0", + "postcss": "^5.0.14", + "postcss-value-parser": "^3.2.3", + "svgo": "^0.7.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-unique-selectors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz", + "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=", + "requires": { + "alphanum-sort": "^1.0.1", + "postcss": "^5.0.4", + "uniqs": "^2.0.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=" + }, + "postcss-zindex": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz", + "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=", + "requires": { + "has": "^1.0.1", + "postcss": "^5.0.4", + "uniqs": "^2.0.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "pretty-bytes": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", + "integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=" + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "requires": { + "renderkid": "^2.0.1", + "utila": "~0.4" + } + }, + "pretty-format": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-20.0.3.tgz", + "integrity": "sha1-Ag41ClYKH+GpjcO+tsz/s4beixQ=", + "requires": { + "ansi-regex": "^2.1.1", + "ansi-styles": "^3.0.0" + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + }, + "prop-types": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", + "requires": { + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + }, + "proxy-addr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", + "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.6.0" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.28.tgz", + "integrity": "sha512-+AqO1Ae+N/4r7Rvchrdm432afjT9hqJRyBN3DQv9At0tPz4hIFSGKbq64fN9dVoCow4oggIIax5/iONx0r9hZw==" + }, + "public-encrypt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", + "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "querystringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", + "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==" + }, + "raf": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", + "requires": { + "performance-now": "^2.1.0" + } + }, + "randomatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", + "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + } + } + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + } + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "react": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/react/-/react-16.4.2.tgz", + "integrity": "sha512-dMv7YrbxO4y2aqnvA7f/ik9ibeLSHQJTI6TrYAenPSaQ6OXfb+Oti+oJiy8WBxgRzlKatYqtCjphTgDSCEiWFg==", + "requires": { + "fbjs": "^0.8.16", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.0" + } + }, + "react-dev-utils": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-5.0.1.tgz", + "integrity": "sha512-+y92rG6pmXt3cpcg/NGmG4w/W309tWNSmyyPL8hCMxuCSg2UP/hUg3npACj2UZc8UKVSXexyLrCnxowizGoAsw==", + "requires": { + "address": "1.0.3", + "babel-code-frame": "6.26.0", + "chalk": "1.1.3", + "cross-spawn": "5.1.0", + "detect-port-alt": "1.1.6", + "escape-string-regexp": "1.0.5", + "filesize": "3.5.11", + "global-modules": "1.0.0", + "gzip-size": "3.0.0", + "inquirer": "3.3.0", + "is-root": "1.0.0", + "opn": "5.2.0", + "react-error-overlay": "^4.0.0", + "recursive-readdir": "2.2.1", + "shell-quote": "1.6.1", + "sockjs-client": "1.1.4", + "strip-ansi": "3.0.1", + "text-table": "0.2.0" + } + }, + "react-dom": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.2.tgz", + "integrity": "sha512-Usl73nQqzvmJN+89r97zmeUpQDKDlh58eX6Hbs/ERdDHzeBzWy+ENk7fsGQ+5KxArV1iOFPT46/VneklK9zoWw==", + "requires": { + "fbjs": "^0.8.16", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.0" + } + }, + "react-error-overlay": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-4.0.0.tgz", + "integrity": "sha512-FlsPxavEyMuR6TjVbSSywovXSEyOg6ZDj5+Z8nbsRl9EkOzAhEIcS+GLoQDC5fz/t9suhUXWmUrOBrgeUvrMxw==" + }, + "react-inlinesvg": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/react-inlinesvg/-/react-inlinesvg-0.8.1.tgz", + "integrity": "sha512-rdeqawsT17tKvY3B9rfHsNUpZ9RpDP7URNLCrv4NifWcIoPcBxAc7Vel1pK7hyAYKgv6DDMaf8x9PB3jyWjW4A==", + "requires": { + "httpplease": "^0.16.4", + "once": "^1.4.0" + } + }, + "react-scripts": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-1.1.4.tgz", + "integrity": "sha512-UVZIujEIT9BGbx+NGvyfS92eOrNIIpqqFi1FP7a0O9l94A/XV7bhPk70SfDKaXZouCX81tFdXo0948DjhCEgGw==", + "requires": { + "autoprefixer": "7.1.6", + "babel-core": "6.26.0", + "babel-eslint": "7.2.3", + "babel-jest": "20.0.3", + "babel-loader": "7.1.2", + "babel-preset-react-app": "^3.1.1", + "babel-runtime": "6.26.0", + "case-sensitive-paths-webpack-plugin": "2.1.1", + "chalk": "1.1.3", + "css-loader": "0.28.7", + "dotenv": "4.0.0", + "dotenv-expand": "4.2.0", + "eslint": "4.10.0", + "eslint-config-react-app": "^2.1.0", + "eslint-loader": "1.9.0", + "eslint-plugin-flowtype": "2.39.1", + "eslint-plugin-import": "2.8.0", + "eslint-plugin-jsx-a11y": "5.1.1", + "eslint-plugin-react": "7.4.0", + "extract-text-webpack-plugin": "3.0.2", + "file-loader": "1.1.5", + "fs-extra": "3.0.1", + "fsevents": "^1.1.3", + "html-webpack-plugin": "2.29.0", + "jest": "20.0.4", + "object-assign": "4.1.1", + "postcss-flexbugs-fixes": "3.2.0", + "postcss-loader": "2.0.8", + "promise": "8.0.1", + "raf": "3.4.0", + "react-dev-utils": "^5.0.1", + "resolve": "1.6.0", + "style-loader": "0.19.0", + "sw-precache-webpack-plugin": "0.11.4", + "url-loader": "0.6.2", + "webpack": "3.8.1", + "webpack-dev-server": "2.9.4", + "webpack-manifest-plugin": "1.3.2", + "whatwg-fetch": "2.0.3" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "eslint": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.10.0.tgz", + "integrity": "sha512-MMVl8P/dYUFZEvolL8PYt7qc5LNdS2lwheq9BYa5Y07FblhcZqFyaUqlS8TW5QITGex21tV4Lk0a3fK8lsJIkA==", + "requires": { + "ajv": "^5.2.0", + "babel-code-frame": "^6.22.0", + "chalk": "^2.1.0", + "concat-stream": "^1.6.0", + "cross-spawn": "^5.1.0", + "debug": "^3.0.1", + "doctrine": "^2.0.0", + "eslint-scope": "^3.7.1", + "espree": "^3.5.1", + "esquery": "^1.0.0", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^9.17.0", + "ignore": "^3.3.3", + "imurmurhash": "^0.1.4", + "inquirer": "^3.0.6", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.9.1", + "json-stable-stringify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "require-uncached": "^1.0.3", + "semver": "^5.3.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "~2.0.1", + "table": "^4.0.1", + "text-table": "~0.2.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "eslint-plugin-react": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz", + "integrity": "sha512-tvjU9u3VqmW2vVuYnE8Qptq+6ji4JltjOjJ9u7VAOxVYkUkyBZWRvNYKbDv5fN+L6wiA+4we9+qQahZ0m63XEA==", + "requires": { + "doctrine": "^2.0.0", + "has": "^1.0.1", + "jsx-ast-utils": "^2.0.0", + "prop-types": "^15.5.10" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsx-ast-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz", + "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", + "requires": { + "array-includes": "^3.0.3" + } + }, + "promise": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.1.tgz", + "integrity": "sha1-5F1osAoXZHttpxG/he1u1HII9FA=", + "requires": { + "asap": "~2.0.3" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "whatwg-fetch": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", + "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" + } + } + }, + "react-scrollchor": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-scrollchor/-/react-scrollchor-6.0.0.tgz", + "integrity": "sha512-Ryc3SoxG/urLrqQ4rFIsj1nWfW9pPGZK30CSlJ670zJWpPFQtVfYOmiZmQUwY96P8L+LcCVmV4q1ZdBlZsuW/Q==", + "requires": { + "requestanimationframe-timer": "1.x.x" + } + }, + "react-shortcut": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/react-shortcut/-/react-shortcut-1.0.6.tgz", + "integrity": "sha1-k9l4AlYSMaBP2bgS//p57wJYo98=", + "requires": { + "bluebird": "3.4.7", + "lodash": "^4.17.4", + "react": "15.4.2", + "react-dom": "^15.4.2" + }, + "dependencies": { + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" + }, + "react": { + "version": "15.4.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.4.2.tgz", + "integrity": "sha1-QfeZGyYYU5K6m66WyIiefgGDl+8=", + "requires": { + "fbjs": "^0.8.4", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0" + } + }, + "react-dom": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", + "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", + "requires": { + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "readable-stream": "^2.0.2", + "set-immediate-shim": "^1.0.1" + } + }, + "recursive-readdir": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.1.tgz", + "integrity": "sha1-kO8jHQd4xc4JPJpI105cVCLROpk=", + "requires": { + "minimatch": "3.0.3" + }, + "dependencies": { + "minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", + "requires": { + "brace-expansion": "^1.0.0" + } + } + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "requires": { + "balanced-match": "^0.4.2", + "math-expression-evaluator": "^1.2.14", + "reduce-function-call": "^1.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + } + } + }, + "reduce-function-call": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", + "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", + "requires": { + "balanced-match": "^0.4.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + } + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "requires": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", + "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", + "dev": true + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "requires": { + "rc": "^1.0.1" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "renderkid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", + "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", + "requires": { + "css-select": "^1.1.0", + "dom-converter": "~0.1", + "htmlparser2": "~3.3.0", + "strip-ansi": "^3.0.0", + "utila": "~0.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=" + } + } + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "^1.4.1" + } + } + } + }, + "requestanimationframe-timer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/requestanimationframe-timer/-/requestanimationframe-timer-1.0.4.tgz", + "integrity": "sha512-5ehtMkIS7RLI/UxjgAEZg6zGLoRdSSzfwjwowpBRImAr8Rbs2pdcS/4tmJ7CAtvYjtO/H+ui96AMkyefzUqUlQ==", + "requires": { + "raf": "^3.4.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "resolve": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz", + "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==", + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "^7.0.5" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "requires": { + "is-promise": "^2.1.0" + } + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=" + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "requires": { + "rx-lite": "*" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sane": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-1.6.0.tgz", + "integrity": "sha1-lhDEUjB6E10pwf3+JUcDQYDEZ3U=", + "requires": { + "anymatch": "^1.3.0", + "exec-sh": "^0.2.0", + "fb-watchman": "^1.8.0", + "minimatch": "^3.0.2", + "minimist": "^1.1.1", + "walker": "~1.0.5", + "watch": "~0.10.0" + }, + "dependencies": { + "bser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bser/-/bser-1.0.2.tgz", + "integrity": "sha1-OBEWlwsqbe6lZG3RXdcnhES1YWk=", + "requires": { + "node-int64": "^0.4.0" + } + }, + "fb-watchman": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-1.9.2.tgz", + "integrity": "sha1-okz0eCf4LTj7Waaa1wt247auc4M=", + "requires": { + "bser": "1.0.2" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "schema-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", + "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "requires": { + "ajv": "^5.0.0" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + }, + "selfsigned": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.3.tgz", + "integrity": "sha512-vmZenZ+8Al3NLHkWnhBQ0x6BkML1eCP2xEi3JE+f3D9wW9fipD9NNJHYtE9XJM4TsPaHGZJIamrSI6MTg1dU2Q==", + "requires": { + "node-forge": "0.7.5" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "requires": { + "semver": "^5.0.3" + } + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + } + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "serviceworker-cache-polyfill": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serviceworker-cache-polyfill/-/serviceworker-cache-polyfill-4.0.0.tgz", + "integrity": "sha1-3hnuc77yGrPAdAo3sz22JGS6ves=" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + } + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sockjs": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.18.tgz", + "integrity": "sha1-2bKJMWyn33dZXvKZ4HXw+TfrQgc=", + "requires": { + "faye-websocket": "^0.10.0", + "uuid": "^2.0.2" + }, + "dependencies": { + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" + } + } + }, + "sockjs-client": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz", + "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=", + "requires": { + "debug": "^2.6.6", + "eventsource": "0.1.6", + "faye-websocket": "~0.11.0", + "inherits": "^2.0.1", + "json3": "^3.3.2", + "url-parse": "^1.1.8" + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "requires": { + "source-map": "^0.5.6" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==" + }, + "spdy": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", + "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "requires": { + "debug": "^2.6.8", + "handle-thing": "^1.2.5", + "http-deceiver": "^1.2.7", + "safe-buffer": "^5.0.1", + "select-hose": "^2.0.0", + "spdy-transport": "^2.0.18" + } + }, + "spdy-transport": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.0.tgz", + "integrity": "sha512-bpUeGpZcmZ692rrTiqf9/2EUakI6/kXX1Rpe0ib/DyOzbiexVfXkw6GnvI9hVGvIwVaUhkaBojjCZwLNRGQg1g==", + "requires": { + "debug": "^2.6.8", + "detect-node": "^2.0.3", + "hpack.js": "^2.1.6", + "obuf": "^1.1.1", + "readable-stream": "^2.2.9", + "safe-buffer": "^5.0.1", + "wbuf": "^1.7.2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + }, + "string-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", + "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", + "requires": { + "strip-ansi": "^3.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "requires": { + "get-stdin": "^4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "style-loader": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.19.0.tgz", + "integrity": "sha512-9mx9sC9nX1dgP96MZOODpGC6l1RzQBITI2D5WJhu+wnbrSYVKLGuy14XJSLVQih/0GFrPpjelt+s//VcZQ2Evw==", + "requires": { + "loader-utils": "^1.0.2", + "schema-utils": "^0.3.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "svgo": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", + "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", + "requires": { + "coa": "~1.0.1", + "colors": "~1.1.2", + "csso": "~2.3.1", + "js-yaml": "~3.7.0", + "mkdirp": "~0.5.1", + "sax": "~1.2.1", + "whet.extend": "~0.9.9" + } + }, + "sw-precache": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/sw-precache/-/sw-precache-5.2.1.tgz", + "integrity": "sha512-8FAy+BP/FXE+ILfiVTt+GQJ6UEf4CVHD9OfhzH0JX+3zoy2uFk7Vn9EfXASOtVmmIVbL3jE/W8Z66VgPSZcMhw==", + "requires": { + "dom-urls": "^1.1.0", + "es6-promise": "^4.0.5", + "glob": "^7.1.1", + "lodash.defaults": "^4.2.0", + "lodash.template": "^4.4.0", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "pretty-bytes": "^4.0.2", + "sw-toolbox": "^3.4.0", + "update-notifier": "^2.3.0" + } + }, + "sw-precache-webpack-plugin": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/sw-precache-webpack-plugin/-/sw-precache-webpack-plugin-0.11.4.tgz", + "integrity": "sha1-ppUBflTu1XVVFJOlGdwdqNotxeA=", + "requires": { + "del": "^2.2.2", + "sw-precache": "^5.1.1", + "uglify-js": "^3.0.13" + } + }, + "sw-toolbox": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/sw-toolbox/-/sw-toolbox-3.6.0.tgz", + "integrity": "sha1-Jt8dHHA0hljk3qKIQxkUm3sxg7U=", + "requires": { + "path-to-regexp": "^1.0.1", + "serviceworker-cache-polyfill": "^4.0.0" + } + }, + "symbol-tree": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=" + }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "requires": { + "ajv": "^5.2.3", + "ajv-keywords": "^2.1.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "tapable": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", + "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=" + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "requires": { + "execa": "^0.7.0" + } + }, + "test-exclude": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", + "integrity": "sha512-qpqlP/8Zl+sosLxBcVKl9vYy26T9NPalxSzzCP/OY6K7j938ui2oKgo+kRZYfxAeIpLqpbVnsHq1tyV70E4lWQ==", + "requires": { + "arrify": "^1.0.1", + "micromatch": "^3.1.8", + "object-assign": "^4.1.0", + "read-pkg-up": "^1.0.1", + "require-main-filename": "^1.0.1" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + }, + "throat": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-3.2.0.tgz", + "integrity": "sha512-/EY8VpvlqJ+sFtLPeOgc8Pl7kQVOWv0woD87KTXVHPIAE842FGT+rokxIhe8xIUP1cfgrkt0as0vDLjDiMtr8w==" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "thunky": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.2.tgz", + "integrity": "sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=" + }, + "time-stamp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-2.0.0.tgz", + "integrity": "sha1-lcakRTDhW6jW9KPsuMOj+sRto1c=" + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" + }, + "timers-browserify": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", + "requires": { + "setimmediate": "^1.0.4" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toposort": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", + "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=" + }, + "tough-cookie": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.2.tgz", + "integrity": "sha512-vahm+X8lSV/KjXziec8x5Vp0OTC9mq8EVCOApIsRAooeuMPSO8aT7PFACYkaL0yZ/3hVqw+8DzhCJwl8H2Ad6w==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "ua-parser-js": { + "version": "0.7.18", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.18.tgz", + "integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA==" + }, + "uglify-js": { + "version": "3.3.28", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.28.tgz", + "integrity": "sha512-68Rc/aA6cswiaQ5SrE979UJcXX+ADA1z33/ZsPd+fbAiVdjZ16OXdbtGO+rJUUBgK6qdf3SOPhQf3K/ybF5Miw==", + "requires": { + "commander": "~2.15.0", + "source-map": "~0.6.1" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "uglifyjs-webpack-plugin": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz", + "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=", + "requires": { + "source-map": "^0.5.6", + "uglify-js": "^2.8.29", + "webpack-sources": "^1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + } + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" + }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==" + }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" + }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", + "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "url-loader": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-0.6.2.tgz", + "integrity": "sha512-h3qf9TNn53BpuXTTcpC+UehiRrl0Cv45Yr/xWayApjw6G8Bg2dGke7rIwDQ39piciWCWrC+WiqLjOh3SUp9n0Q==", + "requires": { + "loader-utils": "^1.0.2", + "mime": "^1.4.1", + "schema-utils": "^0.3.0" + } + }, + "url-parse": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz", + "integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==", + "requires": { + "querystringify": "^2.0.0", + "requires-port": "^1.0.0" + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "requires": { + "prepend-http": "^1.0.1" + } + }, + "urllite": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/urllite/-/urllite-0.5.0.tgz", + "integrity": "sha1-G3u5yj+w25Ug3hE0ZrvPfMNBRRo=", + "requires": { + "xtend": "~4.0.0" + }, + "dependencies": { + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "requires": { + "kind-of": "^6.0.2" + } + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "vendors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz", + "integrity": "sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "requires": { + "indexof": "0.0.1" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "requires": { + "makeerror": "1.0.x" + } + }, + "watch": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz", + "integrity": "sha1-d3mLLaD5kQ1ZXxrOWwwiWFIfIdw=" + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "webpack": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz", + "integrity": "sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==", + "requires": { + "acorn": "^5.0.0", + "acorn-dynamic-import": "^2.0.0", + "ajv": "^5.1.5", + "ajv-keywords": "^2.0.0", + "async": "^2.1.2", + "enhanced-resolve": "^3.4.0", + "escope": "^3.6.0", + "interpret": "^1.0.0", + "json-loader": "^0.5.4", + "json5": "^0.5.1", + "loader-runner": "^2.3.0", + "loader-utils": "^1.1.0", + "memory-fs": "~0.4.1", + "mkdirp": "~0.5.0", + "node-libs-browser": "^2.0.0", + "source-map": "^0.5.3", + "supports-color": "^4.2.1", + "tapable": "^0.2.7", + "uglifyjs-webpack-plugin": "^0.4.6", + "watchpack": "^1.4.0", + "webpack-sources": "^1.0.1", + "yargs": "^8.0.2" + }, + "dependencies": { + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "requires": { + "pify": "^2.0.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "requires": { + "has-flag": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "requires": { + "camelcase": "^4.1.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "read-pkg-up": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^7.0.0" + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "webpack-dev-middleware": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz", + "integrity": "sha512-FCrqPy1yy/sN6U/SaEZcHKRXGlqU0DUaEBL45jkUYoB8foVb6wCnbIJ1HKIx+qUFTW+3JpVcCJCxZ8VATL4e+A==", + "requires": { + "memory-fs": "~0.4.1", + "mime": "^1.5.0", + "path-is-absolute": "^1.0.0", + "range-parser": "^1.0.3", + "time-stamp": "^2.0.0" + } + }, + "webpack-dev-server": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-2.9.4.tgz", + "integrity": "sha512-thrqC0EQEoSjXeYgP6pUXcUCZ+LNrKsDPn+mItLnn5VyyNZOJKd06hUP5vqkYwL8nWWXsii0loSF9NHNccT6ow==", + "requires": { + "ansi-html": "0.0.7", + "array-includes": "^3.0.3", + "bonjour": "^3.5.0", + "chokidar": "^1.6.0", + "compression": "^1.5.2", + "connect-history-api-fallback": "^1.3.0", + "debug": "^3.1.0", + "del": "^3.0.0", + "express": "^4.13.3", + "html-entities": "^1.2.0", + "http-proxy-middleware": "~0.17.4", + "import-local": "^0.1.1", + "internal-ip": "1.2.0", + "ip": "^1.1.5", + "killable": "^1.0.0", + "loglevel": "^1.4.1", + "opn": "^5.1.0", + "portfinder": "^1.0.9", + "selfsigned": "^1.9.1", + "serve-index": "^1.7.2", + "sockjs": "0.3.18", + "sockjs-client": "1.1.4", + "spdy": "^3.4.1", + "strip-ansi": "^3.0.1", + "supports-color": "^4.2.1", + "webpack-dev-middleware": "^1.11.0", + "yargs": "^6.6.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "requires": { + "globby": "^6.1.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "p-map": "^1.1.1", + "pify": "^3.0.0", + "rimraf": "^2.2.8" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "requires": { + "has-flag": "^2.0.0" + } + }, + "yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^4.2.0" + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "requires": { + "camelcase": "^3.0.0" + } + } + } + }, + "webpack-manifest-plugin": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-1.3.2.tgz", + "integrity": "sha512-MX60Bv2G83Zks9pi3oLOmRgnPAnwrlMn+lftMrWBm199VQjk46/xgzBi9lPfpZldw2+EI2S+OevuLIaDuxCWRw==", + "requires": { + "fs-extra": "^0.30.0", + "lodash": ">=3.5 <5" + }, + "dependencies": { + "fs-extra": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0", + "path-is-absolute": "^1.0.0", + "rimraf": "^2.2.8" + } + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, + "webpack-sources": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", + "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "requires": { + "http-parser-js": ">=0.4.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" + }, + "whatwg-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", + "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", + "requires": { + "iconv-lite": "0.4.19" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + } + } + }, + "whatwg-fetch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" + }, + "whatwg-url": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-4.8.0.tgz", + "integrity": "sha1-0pgaqRSMHgCkHFphMRZqtGg7vMA=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + } + } + }, + "whet.extend": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "widest-line": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz", + "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=", + "requires": { + "string-width": "^2.1.1" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "worker-farm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", + "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" + }, + "xml-name-validator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", + "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=" + }, + "xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "requires": { + "camelcase": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + } + } + } + } +} diff --git a/diplomacy/web/package.json b/diplomacy/web/package.json new file mode 100644 index 0000000..43d6617 --- /dev/null +++ b/diplomacy/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "web", + "version": "0.1.0", + "private": true, + "dependencies": { + "@githubprimer/octicons-react": "^8.0.0", + "bootstrap": "^4.1.3", + "fancybox": "^3.0.1", + "jquery": "^3.3.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.2", + "react": "^16.4.2", + "react-dom": "^16.4.2", + "react-inlinesvg": "^0.8.1", + "react-scripts": "1.1.4", + "react-scrollchor": "^6.0.0", + "react-shortcut": "^1.0.6" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "devDependencies": { + "eslint": "^4.19.1", + "eslint-plugin-react": "^7.10.0" + } +} diff --git a/diplomacy/web/public/favicon.ico b/diplomacy/web/public/favicon.ico Binary files differnew file mode 100644 index 0000000..a11777c --- /dev/null +++ b/diplomacy/web/public/favicon.ico diff --git a/diplomacy/web/public/index.html b/diplomacy/web/public/index.html new file mode 100644 index 0000000..20cf957 --- /dev/null +++ b/diplomacy/web/public/index.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="theme-color" content="#000000"> + <!-- + manifest.json provides metadata used when your web app is added to the + homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ + --> + <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> + <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> + <!-- + Notice the use of %PUBLIC_URL% in the tags above. + It will be replaced with the URL of the `public` folder during the build. + Only files inside the `public` folder can be referenced from the HTML. + + Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will + work correctly both with client-side routing and a non-root public URL. + Learn how to configure a non-root public URL by running `npm run build`. + --> + <title>Diplomacy</title> + </head> + <body> + <noscript> + You need to enable JavaScript to run this app. + </noscript> + <div id="root"></div> + <!-- + This HTML file is a template. + If you open it directly in the browser, you will see an empty page. + + You can add webfonts, meta tags, or analytics to this file. + The build step will place the bundled scripts into the <body> tag. + + To begin the development, run `npm start` or `yarn start`. + To create a production bundle, use `npm run build` or `yarn build`. + --> + </body> +</html> diff --git a/diplomacy/web/public/manifest.json b/diplomacy/web/public/manifest.json new file mode 100644 index 0000000..07cc3db --- /dev/null +++ b/diplomacy/web/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Diplomacy", + "name": "Diplomacy Game", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/diplomacy/web/src/diplomacy/client/channel.js b/diplomacy/web/src/diplomacy/client/channel.js new file mode 100644 index 0000000..eb8692a --- /dev/null +++ b/diplomacy/web/src/diplomacy/client/channel.js @@ -0,0 +1,256 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {STRINGS} from "../utils/strings"; +import {UTILS} from "../utils/utils"; +import {REQUESTS} from "../communication/requests"; + +/** Class Channel. **/ +export class Channel { + constructor(connection, username, token) { + this.connection = connection; + this.token = token; + this.username = username; + this.game_id_to_instances = {}; + } + + localJoinGame(joinParameters) { + // Game ID must be known. + if (this.game_id_to_instances.hasOwnProperty(joinParameters.game_id)) { + // If there is a power name, we return associated power game. + if (joinParameters.power_name) + return this.game_id_to_instances[joinParameters.game_id].get(joinParameters.power_name); + // Otherwise, we return current special game (if exists). + return this.game_id_to_instances[joinParameters.game_id].getSpecial(); + } + return null; + } + + _req(name, forcedParameters, localChannelFunction, parameters, game) { + /** Send a request object for given request name with (optional) given forced parameters.. + * If a local channel function is given, it will be used to try retrieving a data + * locally instead of sending a request. If local channel function returns something, this value is returned. + * Otherwise, normal procedure (request sending) is used. Local channel function would be called with + * request parameters passed to channel request method. + * **/ + parameters = Object.assign(parameters || {}, forcedParameters || {}); + const level = REQUESTS.getLevel(name); + if (level === STRINGS.GAME) { + if (!game) + throw new Error('A game object is required to send a game request.'); + parameters.token = this.token; + parameters.game_id = game.local.game_id; + parameters.game_role = game.local.role; + parameters.phase = game.local.phase; + } else { + if (game) + throw new Error('A game object should not be provided for a non-game request.'); + if (level === STRINGS.CHANNEL) + parameters.token = this.token; + } + if (localChannelFunction) { + const localResult = localChannelFunction.apply(this, [parameters]); + if (localResult) + return localResult; + } + const request = REQUESTS.create(name, parameters); + const future = this.connection.send(request, game); + const timeoutID = setTimeout(function () { + if (!future.done()) + future.setException('Timeout reached when trying to send a request ' + name + '/' + request.request_id + '.'); + }, UTILS.REQUEST_TIMEOUT_SECONDS * 1000); + return future.promise().then((result) => { + clearTimeout(timeoutID); + return result; + }); + } + + //// Public channel API. + + createGame(parameters) { + return this._req('create_game', undefined, undefined, parameters, undefined); + } + + getAvailableMaps(parameters) { + return this._req('get_available_maps', undefined, undefined, parameters, undefined); + } + + getPlayablePowers(parameters) { + return this._req('get_playable_powers', undefined, undefined, parameters, undefined); + } + + joinGame(parameters) { + return this._req('join_game', null, this.localJoinGame, parameters, undefined); + } + + listGames(parameters) { + return this._req('list_games', undefined, undefined, parameters, undefined); + } + + getGamesInfo(parameters) { + return this._req('get_games_info', undefined, undefined, parameters, undefined); + } + + // User account API. + + deleteAccount(parameters) { + return this._req('delete_account', undefined, undefined, parameters, undefined); + } + + logout(parameters) { + return this._req('logout', undefined, undefined, parameters, undefined); + } + + // Admin/moderator API. + + makeOmniscient(parameters) { + return this._req('set_grade', { + grade: STRINGS.OMNISCIENT, + grade_update: STRINGS.PROMOTE + }, undefined, parameters, undefined); + } + + removeOmniscient(parameters) { + return this._req('set_grade', { + grade: STRINGS.OMNISCIENT, + grade_update: STRINGS.DEMOTE + }, undefined, parameters, undefined); + } + + promoteAdministrator(parameters) { + return this._req('set_grade', { + grade: STRINGS.ADMIN, + grade_update: STRINGS.PROMOTE + }, undefined, parameters, undefined); + } + + demoteAdministrator(parameters) { + return this._req('set_grade', { + grade: STRINGS.ADMIN, + grade_update: STRINGS.DEMOTE + }, undefined, parameters, undefined); + } + + promoteModerator(parameters) { + return this._req('set_grade', { + grade: STRINGS.MODERATOR, + grade_update: STRINGS.PROMOTE + }, undefined, parameters, undefined); + } + + demoteModerator(parameters) { + return this._req('set_grade', { + grade: STRINGS.MODERATOR, + grade_update: STRINGS.DEMOTE + }, undefined, parameters, undefined); + } + + //// Public game API. + + getAllPossibleOrders(parameters, game) { + return this._req('get_all_possible_orders', undefined, undefined, parameters, game); + } + + getPhaseHistory(parameters, game) { + return this._req('get_phase_history', undefined, undefined, parameters, game); + } + + leaveGame(parameters, game) { + return this._req('leave_game', undefined, undefined, parameters, game); + } + + sendGameMessage(parameters, game) { + return this._req('send_game_message', undefined, undefined, parameters, game); + } + + setOrders(parameters, game) { + return this._req('set_orders', undefined, undefined, parameters, game); + } + + clearCenters(parameters, game) { + return this._req('clear_centers', undefined, undefined, parameters, game); + } + + clearOrders(parameters, game) { + return this._req('clear_orders', undefined, undefined, parameters, game); + } + + clearUnits(parameters, game) { + return this._req('clear_units', undefined, undefined, parameters, game); + } + + wait(parameters, game) { + return this._req('set_wait_flag', {wait: true}, undefined, parameters, game); + } + + noWait(parameters, game) { + return this._req('set_wait_flag', {wait: false}, undefined, parameters, game); + } + + vote(parameters, game) { + return this._req('vote', undefined, undefined, parameters, game); + } + + save(parameters, game) { + return this._req('save_game', undefined, undefined, parameters, game); + } + + synchronize(parameters, game) { + return this._req('synchronize', undefined, undefined, parameters, game); + } + + // Admin/moderator game API. + + deleteGame(parameters, game) { + return this._req('delete_game', undefined, undefined, parameters, game); + } + + kickPowers(parameters, game) { + return this._req('set_dummy_powers', undefined, undefined, parameters, game); + } + + setState(parameters, game) { + return this._req('set_game_state', undefined, undefined, parameters, game); + } + + process(parameters, game) { + return this._req('process_game', undefined, undefined, parameters, game); + } + + querySchedule(parameters, game) { + return this._req('query_schedule', undefined, undefined, parameters, game); + } + + start(parameters, game) { + return this._req('set_game_status', {status: STRINGS.ACTIVE}, undefined, parameters, game); + } + + pause(parameters, game) { + return this._req('set_game_status', {status: STRINGS.PAUSED}, undefined, parameters, game); + } + + resume(parameters, game) { + return this._req('set_game_status', {status: STRINGS.ACTIVE}, undefined, parameters, game); + } + + cancel(parameters, game) { + return this._req('set_game_status', {status: STRINGS.CANCELED}, undefined, parameters, game); + } + + draw(parameters, game) { + return this._req('set_game_status', {status: STRINGS.COMPLETED}, undefined, parameters, game); + } +} diff --git a/diplomacy/web/src/diplomacy/client/connection.js b/diplomacy/web/src/diplomacy/client/connection.js new file mode 100644 index 0000000..8931df5 --- /dev/null +++ b/diplomacy/web/src/diplomacy/client/connection.js @@ -0,0 +1,340 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/*eslint no-unused-vars: ["error", { "args": "none" }]*/ +import {STRINGS} from "../utils/strings"; +import {UTILS} from "../utils/utils"; +import {REQUESTS} from "../communication/requests"; +import {RESPONSES} from "../communication/responses"; +import {NOTIFICATIONS} from "../communication/notifications"; +import {RESPONSE_MANAGERS} from "./response_managers"; +import {NOTIFICATION_MANAGERS} from "./notification_managers"; +import {Future} from "../utils/future"; +import {FutureEvent} from "../utils/future_event"; +import {RequestFutureContext} from "./request_future_context"; +import {Diplog} from "../utils/diplog"; + +class Reconnection { + constructor(connection) { + this.connection = connection; + this.games_phases = {}; + this.n_expected_games = 0; + this.n_synchronized_games = 0; + } + + genSyncCallback(game) { + const reconnection = this; + return ((serverSyncResponse) => { + reconnection.games_phases[game.local.game_id][game.local.game_role] = serverSyncResponse; + ++reconnection.n_synchronized_games; + if (reconnection.n_synchronized_games === reconnection.n_expected_games) + reconnection.syncDone(); + }); + } + + reconnect() { + for (let waitingContext of Object.values(this.connection.requestsWaitingResponses)) + waitingContext.request.re_sent = true; + const lenWaiting = Object.keys(this.connection.requestsWaitingResponses).length; + const lenBefore = Object.keys(this.connection.requestsToSend).length; + Object.assign(this.connection.requestsToSend, this.connection.requestsWaitingResponses); + const lenAfter = Object.keys(this.connection.requestsToSend).length; + if (lenAfter !== lenWaiting + lenBefore) + throw new Error('Programming error.'); + this.connection.requestsWaitingResponses = {}; + + const requestsToSendUpdated = {}; + for (let context of Object.values(this.connection.requestsToSend)) { + if (context.request.name === STRINGS.SYNCHRONIZE) + context.future.setException(new Error('Sync request invalidated for game ID ' + context.request.game_id)); + else + requestsToSendUpdated[context.request.request_id] = context; + } + this.connection.requestsToSend = requestsToSendUpdated; + + for (let channel of Object.values(this.connection.channels)) { + for (let gis of Object.values(channel.game_id_to_instances)) { + for (let game of gis.getGames()) { + const game_id = game.local.game_id; + const game_role = game.local.role; + if (!this.games_phases.hasOwnProperty(game_id)) + this.games_phases[game_id] = {}; + this.games_phases[game_id][game_role] = null; + ++this.n_expected_games; + } + } + } + + if (this.n_expected_games) { + for (let channel of Object.values(this.connection.channels)) + for (let gis of Object.values(channel.game_id_to_instances)) + for (let game of gis.getGames()) + game.synchronize().then(this.genSyncCallback(game)); + } else { + this.syncDone(); + } + } + + syncDone() { + const requestsToSendUpdated = {}; + for (let context of Object.values(this.connection.requestsToSend)) { + let keep = true; + if (REQUESTS.isPhaseDependent(context.request.name)) { + const request_phase = context.request.phase; + const server_phase = this.games_phases[context.request.game_id][context.request.game_role].phase; + if (request_phase !== server_phase) { + context.future.setException(new Error( + 'Game ' + context.request.game_id + ': request ' + context.request.name + + ': request phase ' + request_phase + ' does not match current server game phase ' + + server_phase + '.')); + keep = false; + } + } + if (keep) + requestsToSendUpdated[context.request.request_id] = context; + } + Diplog.info('Keep ' + Object.keys(requestsToSendUpdated).length + '/' + + Object.keys(this.connection.requestsToSend).length + ' old request(s) to send.'); + this.connection.requestsToSend = requestsToSendUpdated; + + for (let context of Object.values(requestsToSendUpdated)) { + this.connection.__write_request(context); + } + + this.connection.isReconnecting.set(); + + Diplog.info('Done reconnection work.'); + } +} + +class ConnectionProcessing { + constructor(connection, logger) { + this.connection = connection; + this.logger = logger || Diplog; + this.isConnected = false; + this.attemptIndex = 1; + this.timeoutID = null; + + this.onSocketOpen = this.onSocketOpen.bind(this); + this.onSocketTimeout = this.onSocketTimeout.bind(this); + this.tryConnect = this.tryConnect.bind(this); + } + + __on_error(error) { + this.connection.isConnecting.set(error); + } + + onSocketOpen(event) { + this.isConnected = true; + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } + // Socket open: set onMessage and onClose callbacks. + this.connection.socket.onmessage = this.connection.onSocketMessage; + this.connection.socket.onclose = this.connection.onSocketClose; + this.connection.currentConnectionProcessing = null; + this.connection.isConnecting.set(); + this.logger.info('Connection succeeds.'); + } + + onSocketTimeout() { + if (!this.isConnected) { + this.connection.socket.close(); + if (this.attemptIndex === UTILS.NB_CONNECTION_ATTEMPTS) { + this.connection.isConnecting.set( + new Error('Connection failed after ' + UTILS.NB_CONNECTION_ATTEMPTS + ' attempts.')); + return; + } + this.logger.warn('Connection failing (attempt ' + this.attemptIndex + '/' + + UTILS.NB_CONNECTION_ATTEMPTS + '), retrying ...'); + ++this.attemptIndex; + setTimeout(this.tryConnect, 0); + } + } + + tryConnect() { + // When opening a socket, we configure only onOpen callback. + // We will configure onMessage and onClose callbacks only when the socket will be effectively open. + try { + this.connection.socket = new WebSocket(this.connection.getUrl()); + this.connection.socket.onopen = this.onSocketOpen; + this.timeoutID = setTimeout(this.onSocketTimeout, UTILS.ATTEMPT_DELAY_SECONDS * 1000); + } catch (error) { + this.__on_error(error); + } + } + + process() { + this.connection.isConnecting.clear(); + if (this.connection.socket) + this.connection.socket.close(); + this.tryConnect(); + return this.connection.isConnecting.wait(); + } + + stop() { + if (!this.isConnected) { + if (this.connection.socket) + this.connection.socket.onopen = null; + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } + } + } +} + +/** Class Connection (like Python class diplomacy.client.connection.Connection). **/ +export class Connection { + constructor(hostname, port, useSSL) { + if (useSSL) + Diplog.info(`Using SSL.`); + this.protocol = useSSL ? 'wss' : 'ws'; + this.hostname = hostname; + this.port = port; + this.socket = null; + this.isConnecting = new FutureEvent(); + this.isReconnecting = new FutureEvent(); + this.channels = {}; + this.requestsToSend = {}; + this.requestsWaitingResponses = {}; + this.currentConnectionProcessing = null; + + // Attribute used to make distinction between a connection + // explicitly closed by client and a connection closed for + // other unexpected reasons (e.g. by server). + this.closed = false; + + this.onSocketMessage = this.onSocketMessage.bind(this); + this.onSocketClose = this.onSocketClose.bind(this); + + this.isReconnecting.set(); + } + + getUrl() { + return this.protocol + '://' + this.hostname + ':' + this.port; + } + + onSocketMessage(messageEvent) { + /** Callback used to manage a socket message string. + * Try-catch block will capture eventual: + * - JSON parsing errors + * - response parsing errors + * - response handling errors + * - notification parsing errors + * - notification handling errors + * **/ + try { + const message = messageEvent.data; + const jsonMessage = JSON.parse(message); + if (!(jsonMessage instanceof Object)) { + Diplog.error('Unable to convert a message to a JSON object.'); + return; + } + if (jsonMessage.request_id) { + const requestID = jsonMessage.request_id; + if (!this.requestsWaitingResponses.hasOwnProperty(requestID)) { + Diplog.error('Unknown request ' + requestID + '.'); + return; + } + const context = this.requestsWaitingResponses[requestID]; + delete this.requestsWaitingResponses[requestID]; + try { + context.future.setResult(RESPONSE_MANAGERS.handleResponse(context, RESPONSES.parse(jsonMessage))); + } catch (error) { + context.future.setException(error); + } + } else if (jsonMessage.hasOwnProperty('notification_id') && jsonMessage.notification_id) + NOTIFICATION_MANAGERS.handleNotification(this, NOTIFICATIONS.parse(jsonMessage)); + else + Diplog.error('Unknown socket message received.'); + } catch (error) { + Diplog.error(error); + } + } + + onSocketClose(closeEvent) { + if (this.closed) + Diplog.info('Disconnected.'); + else { + Diplog.error('Disconnected, trying to reconnect.'); + this.isReconnecting.clear(); + this.__connect().then(() => new Reconnection(this).reconnect()); + } + } + + __connect(logger) { + if (this.currentConnectionProcessing) { + this.currentConnectionProcessing.stop(); + this.currentConnectionProcessing = null; + } + this.currentConnectionProcessing = new ConnectionProcessing(this, logger); + return this.currentConnectionProcessing.process(); + } + + __write_request(requestContext) { + const writeFuture = new Future(); + const request = requestContext.request; + const requestID = request.request_id; + const connection = this; + + const onConnected = () => { + connection.socket.send(JSON.stringify(request)); + connection.requestsWaitingResponses[requestID] = requestContext; + if (connection.requestsToSend.hasOwnProperty(requestID)) { + delete connection.requestsToSend[requestID]; + } + writeFuture.setResult(null); + }; + const onAnyError = (error) => { + if (!connection.requestsToSend.hasOwnProperty(requestID)) { + connection.requestsToSend[requestID] = requestContext; + } + Diplog.info('Error occurred while sending a request ' + requestID); + writeFuture.setException(error); + }; + if (request.name === STRINGS.SYNCHRONIZE) + this.isConnecting.wait().then(onConnected, onAnyError); + else + this.isReconnecting.wait().then(onConnected, onAnyError); + return writeFuture.promise(); + } + + connect(logger) { + Diplog.info('Trying to connect.'); + return this.__connect(logger); + } + + send(request, game = null) { + const requestContext = new RequestFutureContext(request, this, game); + this.__write_request(requestContext); + return requestContext.future; + } + + authenticate(username, password, createUser = false) { + return this.send(REQUESTS.create('sign_in', { + username: username, + password: password, + create_user: createUser + })).promise(); + } + + close() { + this.closed = true; + this.socket.close(); + } +} diff --git a/diplomacy/web/src/diplomacy/client/game_instance_set.js b/diplomacy/web/src/diplomacy/client/game_instance_set.js new file mode 100644 index 0000000..ca92e48 --- /dev/null +++ b/diplomacy/web/src/diplomacy/client/game_instance_set.js @@ -0,0 +1,76 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {STRINGS} from "../utils/strings"; +import {UTILS} from "../utils/utils"; + +export class GameInstanceSet { + constructor(gameID) { + this.__game_id = gameID; + this.__games = {}; + } + + getGames() { + return Object.values(this.__games); + } + + has(role) { + return this.__games.hasOwnProperty(role); + } + + get(role) { + return this.__games[role] || null; + } + + getSpecial() { + if (this.__games.hasOwnProperty(STRINGS.OBSERVER_TYPE)) + return this.__games[STRINGS.OBSERVER_TYPE]; + if (this.__games.hasOwnProperty(STRINGS.OMNISCIENT_TYPE)) + return this.__games[STRINGS.OMNISCIENT_TYPE]; + return null; + } + + remove(role) { + let old = null; + if (this.__games[role]) { + old = this.__games[role]; + delete this.__games[role]; + } + return old; + } + + removeSpecial() { + if (this.__games.hasOwnProperty(STRINGS.OBSERVER_TYPE)) + delete this.__games[STRINGS.OBSERVER_TYPE]; + if (this.__games.hasOwnProperty(STRINGS.OMNISCIENT_TYPE)) + delete this.__games[STRINGS.OMNISCIENT_TYPE]; + } + + add(game) { + if (game.local.game_id !== this.__game_id) + throw new Error('game ID to add does not match game instance set ID.'); + if (this.__games.hasOwnProperty(game.local.role)) + throw new Error('Role already in game instance set.'); + if (!game.local.isPlayerGame() && ( + this.__games.hasOwnProperty(STRINGS.OBSERVER_TYPE) || this.__games.hasOwnProperty(STRINGS.OMNISCIENT_TYPE))) + throw new Error('Previous special game must be removed before adding new one.'); + this.__games[game.local.role] = game; + } + + size() { + return UTILS.javascript.count(this.__games); + } +} diff --git a/diplomacy/web/src/diplomacy/client/network_game.js b/diplomacy/web/src/diplomacy/client/network_game.js new file mode 100644 index 0000000..9999093 --- /dev/null +++ b/diplomacy/web/src/diplomacy/client/network_game.js @@ -0,0 +1,297 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {Channel} from "./channel"; +import {Game} from "../engine/game"; + +/** Class NetworkGame. **/ + +export class NetworkGame { + constructor(channel, serverGameState) { + // Let's use a "local" instance to manage game. + // This will help make distinction between network game request methods and local gme methods + // (e.g. for request set_orders). + this.local = new Game(serverGameState); + this.channel = channel; + this.notificationCallbacks = {}; + this.data = null; + this.local.client = this; + } + + addCallback(notificationName, notificationCallback) { + if (!this.notificationCallbacks.hasOwnProperty(notificationName)) + this.notificationCallbacks[notificationName] = [notificationCallback]; + else if (!this.notificationCallbacks[notificationName].includes(notificationCallback)) + this.notificationCallbacks[notificationName].push(notificationCallback); + } + + clearCallbacks(notificationName) { + if (this.notificationCallbacks.hasOwnProperty(notificationName)) + delete this.notificationCallbacks[notificationName]; + } + + clearAllCallbacks() { + this.notificationCallbacks = {}; + } + + notify(notification) { + if (this.notificationCallbacks.hasOwnProperty(notification.name)) { + for (let callback of this.notificationCallbacks[notification.name]) + setTimeout(() => callback(this, notification), 0); + } + } + + _req(channelMethod, parameters) { + /** Send a game request using given channel request method. **/ + if (!this.channel) + throw new Error('Invalid client game.'); + return channelMethod.apply(this.channel, [parameters, this]); + } + + //// Game requests API. + + getAllPossibleOrders(parameters) { + return this._req(Channel.prototype.getAllPossibleOrders, parameters); + } + + getPhaseHistory(parameters) { + return this._req(Channel.prototype.getPhaseHistory, parameters); + } + + leave(parameters) { + return this._req(Channel.prototype.leaveGame, parameters); + } + + sendGameMessage(parameters) { + return this._req(Channel.prototype.sendGameMessage, parameters); + } + + setOrders(parameters) { + return this._req(Channel.prototype.setOrders, parameters); + } + + clearCenters(parameters) { + return this._req(Channel.prototype.clearCenters, parameters); + } + + clearOrders(parameters) { + return this._req(Channel.prototype.clearOrders, parameters); + } + + clearUnits(parameters) { + return this._req(Channel.prototype.clearUnits, parameters); + } + + wait(parameters) { + return this._req(Channel.prototype.wait, parameters); + } + + noWait(parameters) { + return this._req(Channel.prototype.noWait, parameters); + } + + setWait(wait, parameters) { + return wait ? this.wait(parameters) : this.noWait(parameters); + } + + vote(parameters) { + return this._req(Channel.prototype.vote, parameters); + } + + save(parameters) { + return this._req(Channel.prototype.save, parameters); + } + + synchronize() { + if (!this.channel) + throw new Error('Invalid client game.'); + return Channel.prototype.synchronize.apply(this.channel, [{timestamp: this.local.getLatestTimestamp()}, this]); + } + + // Admin/moderator API. + + remove(parameters) { + return this._req(Channel.prototype.deleteGame, parameters); + } + + kickPowers(parameters) { + return this._req(Channel.prototype.kickPowers, parameters); + } + + setState(parameters) { + return this._req(Channel.prototype.setState, parameters); + } + + process(parameters) { + return this._req(Channel.prototype.process, parameters); + } + + querySchedule(parameters) { + return this._req(Channel.prototype.querySchedule, parameters); + } + + start(parameters) { + return this._req(Channel.prototype.start, parameters); + } + + pause(parameters) { + return this._req(Channel.prototype.pause, parameters); + } + + resume(parameters) { + return this._req(Channel.prototype.resume, parameters); + } + + cancel(parameters) { + return this._req(Channel.prototype.cancel, parameters); + } + + draw(parameters) { + return this._req(Channel.prototype.draw, parameters); + } + + //// Game callbacks setting API. + + addOnClearedCenters(callback) { + this.addCallback('cleared_centers', callback); + } + + addOnClearedOrders(callback) { + this.addCallback('cleared_orders', callback); + } + + addOnClearedUnits(callback) { + this.addCallback('cleared_units', callback); + } + + addOnPowersControllers(callback) { + this.addCallback('powers_controllers', callback); + } + + addOnGameDeleted(callback) { + this.addCallback('game_deleted', callback); + } + + addOnGameMessageReceived(callback) { + this.addCallback('game_message_received', callback); + } + + addOnGameProcessed(callback) { + this.addCallback('game_processed', callback); + } + + addOnGamePhaseUpdate(callback) { + this.addCallback('game_phase_update', callback); + } + + addOnGameStatusUpdate(callback) { + this.addCallback('game_status_update', callback); + } + + addOnOmniscientUpdated(callback) { + this.addCallback('omniscient_updated', callback); + } + + addOnPowerOrdersUpdate(callback) { + this.addCallback('power_orders_update', callback); + } + + addOnPowerOrdersFlag(callback) { + this.addCallback('power_orders_flag', callback); + } + + addOnPowerVoteUpdated(callback) { + this.addCallback('power_vote_updated', callback); + } + + addOnPowerWaitFlag(callback) { + this.addCallback('power_wait_flag', callback); + } + + addOnVoteCountUpdated(callback) { + this.addCallback('vote_count_updated', callback); + } + + addOnVoteUpdated(callback) { + this.addCallback('vote_updated', callback); + } + + //// Game callbacks clearing API. + + clearOnClearedCenters() { + this.clearCallbacks('cleared_centers'); + } + + clearOnClearedOrders() { + this.clearCallbacks('cleared_orders'); + } + + clearOnClearedUnits() { + this.clearCallbacks('cleared_units'); + } + + clearOnPowersControllers() { + this.clearCallbacks('powers_controllers'); + } + + clearOnGameDeleted() { + this.clearCallbacks('game_deleted'); + } + + clearOnGameMessageReceived() { + this.clearCallbacks('game_message_received'); + } + + clearOnGameProcessed() { + this.clearCallbacks('game_processed'); + } + + clearOnGamePhaseUpdate() { + this.clearCallbacks('game_phase_update'); + } + + clearOnGameStatusUpdate() { + this.clearCallbacks('game_status_update'); + } + + clearOnOmniscientUpdated() { + this.clearCallbacks('omniscient_updated'); + } + + clearOnPowerOrdersUpdate() { + this.clearCallbacks('power_orders_update'); + } + + clearOnPowerOrdersFlag() { + this.clearCallbacks('power_orders_flag'); + } + + clearOnPowerVoteUpdated() { + this.clearCallbacks('power_vote_updated'); + } + + clearOnPowerWaitFlag() { + this.clearCallbacks('power_wait_flag'); + } + + clearOnVoteCountUpdated() { + this.clearCallbacks('vote_count_updated'); + } + + clearOnVoteUpdated() { + this.clearCallbacks('vote_updated'); + } +} diff --git a/diplomacy/web/src/diplomacy/client/notification_managers.js b/diplomacy/web/src/diplomacy/client/notification_managers.js new file mode 100644 index 0000000..bbfe862 --- /dev/null +++ b/diplomacy/web/src/diplomacy/client/notification_managers.js @@ -0,0 +1,127 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/*eslint no-unused-vars: ["error", { "args": "none" }]*/ +import {STRINGS} from "../utils/strings"; +import {NOTIFICATIONS} from "../communication/notifications"; +import {Game} from "../engine/game"; + +/** Notification managers. **/ +export const NOTIFICATION_MANAGERS = { + account_deleted: function (channel, notification) { + const connection = channel.connection; + if (connection.channels.hasOwnProperty(channel.token)) + delete channel.connection.channels[channel.token]; + }, + cleared_centers: function (game, notification) { + game.local.clearCenters(notification.power_name); + }, + cleared_orders: function (game, notification) { + game.local.clearOrders(notification.power_name); + }, + cleared_units: function (game, notification) { + game.local.clearUnits(notification.power_name); + }, + powers_controllers: function (game, notification) { + if (game.local.isPlayerGame() && notification.powers[game.local.role] !== game.local.getRelatedPower().getController()) { + game.channel.game_id_to_instances[game.local.game_id].remove(game.local.role); + if (!game.channel.game_id_to_instances[game.local.game_id].size()) + delete game.channel.game_id_to_instances[game.local.game_id]; + } else { + game.local.updatePowersControllers(notification.powers, notification.timestamps); + } + }, + game_deleted: function (game, notification) { + game.channel.game_id_to_instances[game.local.game_id].remove(game.local.role); + }, + game_message_received: function (game, notification) { + game.local.addMessage(notification.message); + }, + game_processed: function (game, notification) { + game.local.extendPhaseHistory(notification.previous_phase_data); + game.local.setPhaseData(notification.current_phase_data); + game.local.clearVote(); + }, + game_phase_update: function (game, notification) { + if (notification.phase_data_type === STRINGS.STATE_HISTORY) + game.local.extendPhaseHistory(notification.phase_data); + else + game.local.setPhaseData(notification.phase_data); + }, + game_status_update: function (game, notification) { + if (game.local.status !== notification.status) { + game.local.setStatus(notification.status); + } + }, + omniscient_updated: function (game, notification) { + if (game.local.isPlayerGame()) return; + if (game.local.isObserverGame()) { + if (notification.grade_update !== STRINGS.PROMOTE || notification.game.role !== STRINGS.OMNISCIENT_TYPE) + throw new Error('Omniscient updated: expected promotion from observer to omniscient'); + } else { + if (notification.grade_update !== STRINGS.DEMOTE || notification.game.role !== STRINGS.OBSERVER_TYPE) + throw new Error('Omniscient updated: expected demotion from omniscient to observer.'); + } + const channel = game.channel; + const oldGame = channel.game_id_to_instances[game.local.game_id].remove(game.local.role); + oldGame.client = null; + game.local = new Game(notification.game); + game.local.client = game; + channel.game_id_to_instances[game.local.game_id].add(game); + }, + power_orders_update: function (game, notification) { + game.local.setOrders(notification.power_name, notification.orders); + }, + power_orders_flag: function (game, notification) { + game.local.getPower(notification.power_name).order_is_set = notification.order_is_set; + }, + power_vote_updated: function (game, notification) { + game.local.assertPlayerGame(); + game.local.getRelatedPower().vote = notification.vote; + }, + power_wait_flag: function (game, notification) { + game.local.setWait(notification.power_name, notification.wait); + }, + vote_count_updated: function (game, notification) { + // Nothing currently done. + }, + vote_updated: function (game, notification) { + game.assertOmniscientGame(); + for (let power_name of notification.vote) { + game.local.getPower(power_name).vote = notification.vote[power_name]; + } + }, + handleNotification: function (connection, notification) { + if (!NOTIFICATION_MANAGERS.hasOwnProperty(notification.name)) + throw new Error('No notification handler available for notification ' + notification.name); + const handler = NOTIFICATION_MANAGERS[notification.name]; + const level = NOTIFICATIONS.levels[notification.name]; + if (!connection.channels.hasOwnProperty(notification.token)) + throw new Error('Unable to find channel related to notification ' + notification.name); + let objectToNotify = connection.channels[notification.token]; + if (level === STRINGS.GAME) { + if (objectToNotify.game_id_to_instances.hasOwnProperty(notification.game_id) + && objectToNotify.game_id_to_instances[notification.game_id].has(notification.game_role)) + objectToNotify = objectToNotify.game_id_to_instances[notification.game_id].get(notification.game_role); + else + throw new Error('Unable to find game instance related to notification ' + + notification.name + '/' + notification.game_id + '/' + notification.game_role); + } + handler(objectToNotify, notification); + if (level === STRINGS.GAME) + objectToNotify.notify(notification); + } +}; diff --git a/diplomacy/web/src/diplomacy/client/request_future_context.js b/diplomacy/web/src/diplomacy/client/request_future_context.js new file mode 100644 index 0000000..16103fb --- /dev/null +++ b/diplomacy/web/src/diplomacy/client/request_future_context.js @@ -0,0 +1,63 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {Future} from "../utils/future"; +import {Channel} from "./channel"; +import {GameInstanceSet} from "./game_instance_set"; +import {NetworkGame} from "./network_game"; + +/** Class RequestFutureContext. **/ +export class RequestFutureContext { + constructor(request, connection, game = null) { + this.request = request; + this.connection = connection; + this.game = game; + this.future = new Future(); + } + + getRequestId() { + return this.request.request_id; + } + + getChannel() { + return this.connection.channels[this.request.token]; + } + + newChannel(username, token) { + const channel = new Channel(this.connection, username, token); + this.connection.channels[token] = channel; + return channel; + } + + newGame(received_game) { + const channel = this.getChannel(); + const game = new NetworkGame(channel, received_game); + if (!channel.game_id_to_instances.hasOwnProperty(game.local.game_id)) + channel.game_id_to_instances[game.local.game_id] = new GameInstanceSet(game.local.game_id); + channel.game_id_to_instances[game.local.game_id].add(game); + return game; + } + + removeChannel() { + delete this.connection.channels[this.request.token]; + } + + deleteGame() { + const channel = this.getChannel(); + if (channel.game_id_to_instances.hasOwnProperty(this.request.game_id)) + delete channel.game_id_to_instances[this.request.game_id]; + } +} diff --git a/diplomacy/web/src/diplomacy/client/response_managers.js b/diplomacy/web/src/diplomacy/client/response_managers.js new file mode 100644 index 0000000..ba45938 --- /dev/null +++ b/diplomacy/web/src/diplomacy/client/response_managers.js @@ -0,0 +1,118 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/*eslint no-unused-vars: ["error", { "args": "none" }]*/ +import {RESPONSES} from "../communication/responses"; + +/** Default response manager. **/ +function defaultResponseManager(context, response) { + if (RESPONSES.isOk(response)) + return null; + if (RESPONSES.isUniqueData(response)) + return response.data; + return response; +} + +/** Response managers. **/ +export const RESPONSE_MANAGERS = { + get_all_possible_orders: defaultResponseManager, + get_available_maps: defaultResponseManager, + get_playable_powers: defaultResponseManager, + list_games: defaultResponseManager, + get_games_info: defaultResponseManager, + process_game: defaultResponseManager, + query_schedule: defaultResponseManager, + save_game: defaultResponseManager, + set_dummy_powers: defaultResponseManager, + set_grade: defaultResponseManager, + synchronize: defaultResponseManager, + create_game: function (context, response) { + return context.newGame(response.data); + }, + delete_account: function (context, response) { + context.removeChannel(); + }, + delete_game: function (context, response) { + context.deleteGame(); + }, + get_phase_history: function (context, response) { + for (let phaseData of response.data) { + context.game.local.extendPhaseHistory(phaseData); + } + return response.data; + }, + join_game: function (context, response) { + return context.newGame(response.data); + }, + leave_game: function (context, response) { + context.deleteGame(); + }, + logout: function (context, response) { + context.removeChannel(); + }, + send_game_message: function (context, response) { + const message = context.request.message; + message.time_sent = response.data; + context.game.local.addMessage(message); + }, + set_game_state: function (context, response) { + context.game.local.setPhaseData({ + name: context.request.state.name, + state: context.request.state, + orders: context.request.orders, + messages: context.request.messages, + results: context.request.results + }); + }, + set_game_status: function (context, response) { + context.game.local.setStatus(context.request.status); + }, + set_orders: function (context, response) { + const orders = context.request.orders; + if (context.game.local.isPlayerGame(context.request.game_role)) + context.game.local.setOrders(context.request.game_role, orders); + else + context.game.local.setOrders(context.request.power_name, orders); + }, + clear_orders: function (context, response) { + context.game.local.clearOrders(context.request.power_name); + }, + clear_units: function (context, response) { + context.game.local.clearUnits(context.request.power_name); + }, + clear_centers: function (context, response) { + context.game.local.clearCenters(context.request.power_name); + }, + set_wait_flag: function (context, response) { + const wait = context.request.wait; + if (context.game.local.isPlayerGame(context.request.game_role)) + context.game.local.setWait(context.request.game_role, wait); + else + context.game.local.setWait(context.request.power_name, wait); + }, + vote: function (context, response) { + context.game.local.getRelatedPower().vote = context.request.vote; + }, + sign_in: function (context, response) { + return context.newChannel(context.request.username, response.data); + }, + handleResponse: function (context, response) { + if (!RESPONSE_MANAGERS.hasOwnProperty(context.request.name)) + throw new Error('No response handler available for request ' + context.request.name); + const handler = RESPONSE_MANAGERS[context.request.name]; + return handler(context, response); + } +}; diff --git a/diplomacy/web/src/diplomacy/communication/notifications.js b/diplomacy/web/src/diplomacy/communication/notifications.js new file mode 100644 index 0000000..149df09 --- /dev/null +++ b/diplomacy/web/src/diplomacy/communication/notifications.js @@ -0,0 +1,56 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {STRINGS} from "../utils/strings"; + +/** Notifications. **/ +export const NOTIFICATIONS = { + levels: { + // Notification name to notification level ('channel' or 'game'). + account_deleted: STRINGS.CHANNEL, + cleared_centers: STRINGS.GAME, + cleared_orders: STRINGS.GAME, + cleared_units: STRINGS.GAME, + game_deleted: STRINGS.GAME, + game_message_received: STRINGS.GAME, + game_processed: STRINGS.GAME, + game_phase_update: STRINGS.GAME, + game_status_update: STRINGS.GAME, + omniscient_updated: STRINGS.GAME, + power_orders_flag: STRINGS.GAME, + power_orders_update: STRINGS.GAME, + power_vote_updated: STRINGS.GAME, + power_wait_flag: STRINGS.GAME, + powers_controllers: STRINGS.GAME, + vote_count_updated: STRINGS.GAME, + vote_updated: STRINGS.GAME, + }, + parse: function (jsonObject) { + if (!jsonObject.hasOwnProperty('name')) + throw new Error('No name field in expected notification object.'); + if (!jsonObject.hasOwnProperty('token')) + throw new Error('No token field in expected notification object.'); + if (!NOTIFICATIONS.levels.hasOwnProperty(jsonObject.name)) + throw new Error('Invalid notification name ' + jsonObject.name); + if (NOTIFICATIONS.levels[jsonObject.name] === STRINGS.GAME) { + if (!jsonObject.hasOwnProperty('game_id')) + throw new Error('No game_id field in expected game notification object.'); + if (!jsonObject.hasOwnProperty('game_role')) + throw new Error('No game_role field in expected game notification object.'); + } + return jsonObject; + } +}; diff --git a/diplomacy/web/src/diplomacy/communication/requests.js b/diplomacy/web/src/diplomacy/communication/requests.js new file mode 100644 index 0000000..6902b5f --- /dev/null +++ b/diplomacy/web/src/diplomacy/communication/requests.js @@ -0,0 +1,120 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {STRINGS} from "../utils/strings"; +import {UTILS} from "../utils/utils"; + +/** Requests. **/ +export const REQUESTS = { + /** Abstract request models, mapping base request field names with default values. + * Every request has at least basic fields. + * Every channel request has at least basic and channel fields. + * Every game request has at least basic, channel and game fields. **/ + abstract: { + basic: {request_id: null, name: null, re_sent: false}, + channel: {token: null}, + game: {game_id: null, game_role: null, phase: null}, + }, + + /** Request models. A request model is defined with: + * - request level: either null, 'channel' or 'game' + * - request model itself: a dictionary mapping each request field name with a default value. + * - request phase dependent (optional, for game requests): boolean (default, true) + * **/ + models: { + sign_in: {level: null, model: {username: null, password: null, create_user: null}}, + create_game: { + level: STRINGS.CHANNEL, + model: { + game_id: null, n_controls: null, deadline: 300, registration_password: null, + power_name: null, state: null, map_name: 'standard', rules: null + } + }, + delete_account: {level: STRINGS.CHANNEL, model: {username: null}}, + get_all_possible_orders: {level: STRINGS.GAME, model: {}}, + get_available_maps: {level: STRINGS.CHANNEL, model: {}}, + get_playable_powers: {level: STRINGS.CHANNEL, model: {game_id: null}}, + join_game: {level: STRINGS.CHANNEL, model: {game_id: null, power_name: null, registration_password: null}}, + list_games: { + level: STRINGS.CHANNEL, + model: {game_id: null, status: null, map_name: null, include_protected: true, for_omniscience: false} + }, + get_games_info: {level: STRINGS.CHANNEL, model: {games: null}}, + logout: {level: STRINGS.CHANNEL, model: {}}, + set_grade: {level: STRINGS.CHANNEL, model: {grade: null, grade_update: null, username: null, game_id: null}}, + clear_centers: {level: STRINGS.GAME, model: {power_name: null}}, + clear_orders: {level: STRINGS.GAME, model: {power_name: null}}, + clear_units: {level: STRINGS.GAME, model: {power_name: null}}, + delete_game: {level: STRINGS.GAME, phase_dependent: false, model: {}}, + get_phase_history: { + level: STRINGS.GAME, + phase_dependent: false, + model: {from_phase: null, to_phase: null} + }, + leave_game: {level: STRINGS.GAME, model: {}}, + process_game: {level: STRINGS.GAME, model: {}}, + query_schedule: {level: STRINGS.GAME, model: {}}, + send_game_message: {level: STRINGS.GAME, model: {message: null}}, + set_dummy_powers: {level: STRINGS.GAME, model: {username: null, power_names: null}}, + set_game_state: {level: STRINGS.GAME, model: {state: null, orders: null, results: null, messages: null}}, + set_game_status: {level: STRINGS.GAME, model: {status: null}}, + set_orders: {level: STRINGS.GAME, model: {power_name: null, orders: null}}, + set_wait_flag: {level: STRINGS.GAME, model: {power_name: null, wait: null}}, + synchronize: {level: STRINGS.GAME, phase_dependent: false, model: {timestamp: null}}, + vote: {level: STRINGS.GAME, model: {vote: null}}, + save_game: {level: STRINGS.GAME, model: {}}, + }, + + isPhaseDependent: function (name) { + if (!REQUESTS.models.hasOwnProperty(name)) + throw new Error('Unknown request name ' + name); + const model = REQUESTS.models[name]; + return (model.level === STRINGS.GAME && (!model.hasOwnProperty('phase_dependent') || model.phase_dependent)); + }, + + /** Return request level for given request name. Either null, 'channel' or 'game'. **/ + getLevel: function (name) { + if (!REQUESTS.models.hasOwnProperty(name)) + throw new Error('Unknown request name ' + name); + return REQUESTS.models[name].level; + }, + + /** Create a request object for given request name with given request field values. + * `parameters` is a dictionary mapping some request fields with values. + * Parameters may not contain values for optional request fields. See Python module + * diplomacy.communication.requests about requests definitions, required and optional fields. + * **/ + create: function (name, parameters) { + if (!REQUESTS.models.hasOwnProperty(name)) + throw new Error('Unknown request name ' + name); + let models = null; + const definition = REQUESTS.models[name]; + if (definition.level === STRINGS.GAME) + models = [{}, definition.model, REQUESTS.abstract.game, REQUESTS.abstract.channel]; + else if (definition.level === STRINGS.CHANNEL) + models = [{}, definition.model, REQUESTS.abstract.channel]; + else + models = [{}, definition.model]; + models.push(REQUESTS.abstract.basic); + models.push({name: name}); + const request = Object.assign.apply(null, models); + if (parameters) for (let parameter of Object.keys(parameters)) if (request.hasOwnProperty(parameter)) + request[parameter] = parameters[parameter]; + if (!request.request_id) + request.request_id = UTILS.createID(); + return request; + }, +}; diff --git a/diplomacy/web/src/diplomacy/communication/responses.js b/diplomacy/web/src/diplomacy/communication/responses.js new file mode 100644 index 0000000..7c87c67 --- /dev/null +++ b/diplomacy/web/src/diplomacy/communication/responses.js @@ -0,0 +1,42 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {STRINGS} from "../utils/strings"; + +/** Responses. **/ +export const RESPONSES = { + names: new Set([ + 'error', 'ok', 'data_game_phase', 'data_token', 'data_maps', 'data_power_names', 'data_games', + 'data_possible_orders', 'data_game_info', 'data_time_stamp', 'data_game_phases', 'data_game', + 'data_game_schedule', 'data_saved_game' + ]), + parse: function (jsonObject) { + if (!jsonObject.hasOwnProperty('name')) + throw new Error('No name field in expected response object'); + if (!RESPONSES.names.has(jsonObject.name)) + throw new Error('Invalid response name ' + jsonObject.name); + if (jsonObject.name === STRINGS.ERROR) + throw new Error(jsonObject.name + ': ' + jsonObject.message); + return jsonObject; + }, + isOk: function (response) { + return response.name === STRINGS.OK; + }, + isUniqueData: function (response) { + // Expected only 3 fields: name, request_id, data. + return (response.hasOwnProperty('data') && Object.keys(response).length === 3); + } +}; diff --git a/diplomacy/web/src/diplomacy/engine/game.js b/diplomacy/web/src/diplomacy/engine/game.js new file mode 100644 index 0000000..cc9803e --- /dev/null +++ b/diplomacy/web/src/diplomacy/engine/game.js @@ -0,0 +1,507 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {UTILS} from "../utils/utils"; +import {STRINGS} from "../utils/strings"; +import {SortedDict} from "../utils/sorted_dict"; +import {Power} from "./power"; +import {Message} from "./message"; + +export function comparablePhase(shortPhaseName) { + /** Return a unique integer corresponding to given short phase name, so that + * phases can be compared using such integers. + * **/ + // Phase 'FORMING' is assumed to be the smallest phase. + if (shortPhaseName === 'FORMING') + return 0; + // Phase 'COMPLETED' is assumed to be the greatest phase. + if (shortPhaseName === 'COMPLETED') + return Number.MAX_SAFE_INTEGER; + if (shortPhaseName.length !== 6) + throw new Error(`Invalid short phase name: ${shortPhaseName}`); + const seasonOrder = {S: 0, F: 1, W: 2}; + const stepOrder = {M: 0, R: 1, A: 2}; + const phaseSeason = shortPhaseName[0]; + const phaseYear = parseInt(shortPhaseName.substring(1, 5), 10); + const phaseStep = shortPhaseName[5]; + if (isNaN(phaseYear)) + throw new Error(`Unable to parse phase year from ${shortPhaseName}`); + if (!seasonOrder.hasOwnProperty(phaseSeason)) + throw new Error(`Unable to parse phase season from ${shortPhaseName}`); + if (!stepOrder.hasOwnProperty(phaseStep)) + throw new Error(`Unable to parse phase step from ${shortPhaseName}`); + return (phaseYear * 100) + (seasonOrder[phaseSeason] * 10) + stepOrder[phaseStep]; +} + +export class Game { + constructor(gameData) { + ////// Instead of using: `Object.assign(this, gameState)`, + ////// we set each field separately to let IDE know all attributes expected for Game class. + //// We first check gameState. + // These fields must not be null. + + const nonNullFields = [ + 'game_id', 'map_name', 'messages', 'role', 'rules', 'status', 'timestamp_created', 'deadline', + 'message_history', 'order_history', 'state_history' + ]; + // These fields may be null. + const nullFields = ['n_controls', 'registration_password']; + // All fields are required. + for (let field of nonNullFields) + if (!gameData.hasOwnProperty(field) || gameData[field] == null) + throw new Error('Game: given state must have field `' + field + '` with non-null value.'); + for (let field of nullFields) + if (!gameData.hasOwnProperty(field)) + throw new Error('Game: given state must have field `' + field + '`.'); + + this.game_id = gameData.game_id; + this.map_name = gameData.map_name; + this.messages = new SortedDict(gameData instanceof Game ? null : gameData.messages, parseInt); + + // {short phase name => state} + this.state_history = gameData instanceof Game ? gameData.state_history : new SortedDict(gameData.state_history, comparablePhase); + // {short phase name => {power name => [orders]}} + this.order_history = gameData instanceof Game ? gameData.order_history : new SortedDict(gameData.order_history, comparablePhase); + // {short phase name => {unit => [results]}} + this.result_history = gameData instanceof Game ? gameData.result_history : new SortedDict(gameData.result_history, comparablePhase); + // {short phase name => {message.time_sent => message}} + if (gameData instanceof Game) { + this.message_history = gameData.message_history; + } else { + this.message_history = new SortedDict(null, comparablePhase); + for (let entry of Object.entries(gameData.message_history)) { + const shortPhaseName = entry[0]; + const phaseMessages = entry[1]; + const sortedPhaseMessages = new SortedDict(phaseMessages, parseInt); + this.message_history.put(shortPhaseName, sortedPhaseMessages); + } + } + + this.role = gameData.role; + this.rules = gameData.rules; + this.status = gameData.status; + this.timestamp_created = gameData.timestamp_created; + this.deadline = gameData.deadline; + this.n_controls = gameData.n_controls; + this.registration_password = gameData.registration_password; + this.observer_level = gameData.observer_level; + this.controlled_powers = gameData.controlled_powers; + this.result = gameData.result || null; + + this.phase = gameData.phase_abbr || null; // phase abbreviation + + this.powers = {}; + if (gameData.powers) { + for (let entry of Object.entries(gameData.powers)) { + const power_name = entry[0]; + const powerState = entry[1]; + if (powerState instanceof Power) { + this.powers[power_name] = powerState.copy(); + } else { + this.powers[power_name] = new Power(power_name, (this.isPlayerGame() ? power_name : this.role), this); + this.powers[power_name].setState(powerState); + } + } + } else if(this.state_history.size()) { + const lastState = this.state_history.lastValue(); + if (lastState.units) { + for (let powerName of Object.keys(lastState.units)) { + this.powers[powerName] = new Power(powerName, (this.isPlayerGame() ? powerName : this.role), this); + } + } + } + + this.note = gameData.note; + this.builds = null; + + // {location => [possible orders]} + this.possibleOrders = null; + // {power name => [orderable locations]} + this.orderableLocations = null; + this.ordersTree = null; + // {loc => order type} + this.orderableLocToTypes = null; + this.client = null; // Used as pointer to a NetworkGame. + } + + get n_players() { + return this.countControlledPowers(); + } + + static createOrdersTree(possibleOrders, tree, locToTypes) { + for (let orders of Object.values(possibleOrders)) { + for (let order of orders) { + // We ignore WAIVE order. + if (order === 'WAIVE') + continue; + const pieces = order.split(/ +/); + const thirdPiece = pieces[2]; + const lastPiece = pieces[pieces.length - 1]; + switch (thirdPiece) { + case 'H': + // 'H', unit + UTILS.javascript.extendTreeValue(tree, ['H'], `${pieces[0]} ${pieces[1]}`); + UTILS.javascript.extendArrayWithUniqueValues(locToTypes, pieces[1], 'H'); + break; + case '-': + // 'M', unit, province + // 'V', unit, province + UTILS.javascript.extendTreeValue(tree, ['M', `${pieces[0]} ${pieces[1]}`, pieces[3]], (lastPiece === 'VIA' ? 'V' : 'M')); + UTILS.javascript.extendArrayWithUniqueValues(locToTypes, pieces[1], 'M'); + break; + case 'S': + // 'S', supporter unit, supported unit, province + UTILS.javascript.extendTreeValue(tree, ['S', `${pieces[0]} ${pieces[1]}`, `${pieces[3]} ${pieces[4]}`], lastPiece); + UTILS.javascript.extendArrayWithUniqueValues(locToTypes, pieces[1], 'S'); + break; + case 'C': + // 'C', convoyer unit, convoyed unit, province + UTILS.javascript.extendTreeValue(tree, ['C', `${pieces[0]} ${pieces[1]}`, `${pieces[3]} ${pieces[4]}`], pieces[6]); + UTILS.javascript.extendArrayWithUniqueValues(locToTypes, pieces[1], 'C'); + break; + case 'R': + // 'R', unit, province + UTILS.javascript.extendTreeValue(tree, ['R', `${pieces[0]} ${pieces[1]}`], pieces[3]); + UTILS.javascript.extendArrayWithUniqueValues(locToTypes, pieces[1], 'R'); + break; + case 'D': + // D, unit + UTILS.javascript.extendTreeValue(tree, ['D'], `${pieces[0]} ${pieces[1]}`); + UTILS.javascript.extendArrayWithUniqueValues(locToTypes, pieces[1], 'D'); + break; + case 'B': + // B, unit + UTILS.javascript.extendTreeValue(tree, [pieces[0]], pieces[1]); + UTILS.javascript.extendArrayWithUniqueValues(locToTypes, pieces[1], pieces[0]); + break; + default: + throw new Error(`Unable to parse order: ${order}`); + } + } + } + } + + extendPhaseHistory(phaseData) { + if (this.state_history.contains(phaseData.name)) throw new Error(`Phase ${phaseData.phase} already in state history.`); + if (this.message_history.contains(phaseData.name)) throw new Error(`Phase ${phaseData.phase} already in message history.`); + if (this.order_history.contains(phaseData.name)) throw new Error(`Phase ${phaseData.phase} already in order history.`); + if (this.result_history.contains(phaseData.name)) throw new Error(`Phase ${phaseData.phase} already in result history.`); + this.state_history.put(phaseData.name, phaseData.state); + this.order_history.put(phaseData.name, phaseData.orders); + this.result_history.put(phaseData.name, phaseData.results); + this.message_history.put(phaseData.name, new SortedDict(phaseData.messages, parseInt)); + } + + addMessage(message) { + message = new Message(message); + if (!message.time_sent) + throw new Error('No time sent for given message.'); + if (this.messages.hasOwnProperty(message.time_sent)) + throw new Error('There is already a message with time sent ' + message.time_sent + ' in message history.'); + if (this.isPlayerGame() && !message.isGlobal() && this.role !== message.sender && this.role !== message.recipient) + throw new Error('Given message is not related to current player ' + this.role); + this.messages.put(message.time_sent, message); + } + + assertPlayerGame(powerName) { + if (!this.isPlayerGame(powerName)) + throw new Error('Expected a player game' + (powerName ? (' ' + powerName) : '') + ', got role ' + this.role + '.'); + } + + assertObserverGame() { + if (!this.isObserverGame()) + throw new Error('Expected an observer game, got role ' + this.role + '.'); + } + + assertOmniscientGame() { + if (!this.isOmniscientGame()) + throw new Error('Expected an omniscient game, got role ' + this.role + '.'); + } + + clearCenters(powerName) { + for (let power_name of Object.keys(this.powers)) { + if (!powerName || power_name === powerName) + this.powers[power_name].clearCenters(); + } + } + + clearOrders(powerName) { + for (let power_name of Object.keys(this.powers)) + if (!powerName || power_name === powerName) + this.powers[power_name].clearOrders(); + } + + clearUnits(powerName) { + for (let power_name of Object.keys(this.powers)) { + if (!powerName || power_name === powerName) + this.powers[power_name].clearUnits(); + } + } + + clearVote() { + for (let power_name of Object.keys(this.powers)) + this.powers[power_name].vote = 'neutral'; + } + + countControlledPowers() { + let count = 0; + for (let power of Object.values(this.powers)) + count += power.isControlled() ? 1 : 0; + return count; + } + + extendStateHistory(state) { + if (this.state_history.contains(state.name)) + throw new Error('There is already a state with phase ' + state.name + ' in state history.'); + this.state_history.put(state.name, state); + } + + getLatestTimestamp() { + return Math.max( + this.timestamp_created, + (this.state_history.size() ? this.state_history.lastValue().timestamp : 0), + (this.messages.size() ? this.messages.lastKey() : 0) + ); + } + + getPower(name) { + return this.powers.hasOwnProperty(name) ? this.powers[name] : null; + } + + getRelatedPower() { + return this.getPower(this.role); + } + + hasPower(powerName) { + return this.powers.hasOwnProperty(powerName); + } + + isPlayerGame(powerName) { + return (this.hasPower(this.role) && (!powerName || this.role === powerName)); + } + + isObserverGame() { + return this.role === STRINGS.OBSERVER_TYPE; + } + + isOmniscientGame() { + return this.role === STRINGS.OMNISCIENT_TYPE; + } + + isRealTime() { + return this.rules.includes('REAL_TIME'); + } + + isNoCheck() { + return this.rules.includes('NO_CHECK'); + } + + setPhaseData(phaseData) { + this.setState(phaseData.state); + this.clearOrders(); + for (let entry of Object.entries(phaseData.orders)) { + if (entry[1]) + this.setOrders(entry[0], entry[1]); + } + this.messages = phaseData.messages instanceof SortedDict ? phaseData.messages : new SortedDict(phaseData.messages, parseInt); + } + + setState(state) { + this.result = state.result || null; + this.note = state.note || null; + this.phase = state.name; + if (state.units) { + for (let power_name of Object.keys(state.units)) { + if (this.powers.hasOwnProperty(power_name)) { + const units = state.units[power_name]; + const power = this.powers[power_name]; + power.retreats = {}; + power.units = []; + for (let unit of units) { + if (unit.charAt(0) === '*') + power.retreats[unit.substr(1)] = {}; + else + power.units.push(unit); + } + } + } + } + if (state.centers) + for (let power_name of Object.keys(state.centers)) + if (this.powers.hasOwnProperty(power_name)) + this.powers[power_name].centers = state.centers[power_name]; + if (state.homes) + for (let power_name of Object.keys(state.homes)) + if (this.powers.hasOwnProperty(power_name)) + this.powers[power_name].homes = state.homes[power_name]; + if (state.influence) + for (let power_name of Object.keys(state.influence)) + if (this.powers.hasOwnProperty(power_name)) + this.powers[power_name].influence = state.influence[power_name]; + if (state.civil_disorder) + for (let power_name of Object.keys(state.civil_disorder)) + if (this.powers.hasOwnProperty(power_name)) + this.powers[power_name].civil_disorder = state.civil_disorder[power_name]; + if (state.builds) + this.builds = state.builds; + } + + setStatus(status) { + this.status = status; + } + + setOrders(powerName, orders) { + if (this.powers.hasOwnProperty(powerName) && (!this.isPlayerGame() || this.isPlayerGame(powerName))) + this.powers[powerName].setOrders(orders); + } + + setWait(powerName, wait) { + if (this.powers.hasOwnProperty(powerName)) { + this.powers[powerName].wait = wait; + } + } + + updateDummyPowers(dummyPowers) { + for (let dummyPowerName of dummyPowers) if (this.powers.hasOwnProperty(dummyPowerName)) + this.powers[dummyPowerName].setDummy(); + } + + updatePowersControllers(controllers, timestamps) { + for (let entry of Object.entries(controllers)) { + this.getPower(entry[0]).updateController(entry[1], timestamps[entry[0]]); + } + } + + cloneAt(pastPhase) { + if (pastPhase !== null && this.state_history.contains(pastPhase)) { + const messages = this.message_history.get(pastPhase); + const orders = this.order_history.get(pastPhase); + const state = this.state_history.get(pastPhase); + const game = new Game(this); + game.setPhaseData({ + name: pastPhase, + state: state, + orders: orders, + messages: messages + }); + return game; + } + return null; + } + + getPhaseType() { + if (this.phase === null || this.phase === 'FORMING' || this.phase === 'COMPLETED') + return null; + return this.phase[this.phase.length - 1]; + } + + getControllablePowers() { + if (!this.isObserverGame()) { + if (this.isOmniscientGame()) + return Object.keys(this.powers); + return [this.role]; + } + return []; + } + + getMessageChannels() { + const messageChannels = {}; + let messages = this.messages; + if (!messages.size() && this.message_history.contains(this.phase)) + messages = this.message_history.get(this.phase); + if (this.isPlayerGame()) { + for (let message of messages.values()) { + let protagonist = null; + if (message.sender === this.role || message.recipient === 'GLOBAL') + protagonist = message.recipient; + else if (message.recipient === this.role) + protagonist = message.sender; + if (!messageChannels.hasOwnProperty(protagonist)) + messageChannels[protagonist] = []; + messageChannels[protagonist].push(message); + } + } else { + messageChannels['messages'] = messages.values(); + } + return messageChannels; + } + + markAllMessagesRead() { + for (let message of this.messages.values()) { + if (message.sender !== this.role) + message.read = true; + } + } + + setPossibleOrders(possibleOrders) { + this.possibleOrders = possibleOrders.possible_orders; + this.orderableLocations = possibleOrders.orderable_locations; + this.ordersTree = {}; + this.orderableLocToTypes = {}; + Game.createOrdersTree(this.possibleOrders, this.ordersTree, this.orderableLocToTypes); + } + + getOrderTypeToLocs(powerName) { + const typeToLocs = {}; + for (let loc of this.orderableLocations[powerName]) { + // loc may be a coastal province. In such case, we must check province coasts too. + const associatedLocs = []; + for (let possibleLoc of Object.keys(this.orderableLocToTypes)) { + if (possibleLoc.substring(0, 3).toUpperCase() === loc.toUpperCase()) { + associatedLocs.push(possibleLoc); + } + } + for (let associatedLoc of associatedLocs) { + const orderTypes = this.orderableLocToTypes[associatedLoc]; + for (let orderType of orderTypes) { + if (!typeToLocs.hasOwnProperty(orderType)) + typeToLocs[orderType] = [associatedLoc]; + else + typeToLocs[orderType].push(associatedLoc); + } + } + } + return typeToLocs; + } + + _build_sites(power) { + const homes = this.rules.includes('BUILD_ANY') ? power.centers : power.homes; + const occupiedLocations = []; + for (let p of Object.values(this.powers)) { + for (let unit of p.units) { + occupiedLocations.push(unit.substring(2, 5)); + } + } + const buildSites = []; + for (let h of homes) { + if (power.centers.includes(h) && !occupiedLocations.includes(h)) + buildSites.push(h); + } + return buildSites; + } + + getBuildsCount(powerName) { + if (this.getPhaseType() !== 'A') + return 0; + const power = this.powers[powerName]; + let buildCount = power.centers.length - power.units.length; + if (buildCount > 0) { + const buildSites = this._build_sites(power); + buildCount = Math.min(buildSites.length, buildCount); + } + return buildCount; + } +} diff --git a/diplomacy/web/src/diplomacy/engine/message.js b/diplomacy/web/src/diplomacy/engine/message.js new file mode 100644 index 0000000..c91ab6f --- /dev/null +++ b/diplomacy/web/src/diplomacy/engine/message.js @@ -0,0 +1,34 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +const GLOBAL = 'GLOBAL'; + +export class Message { + + constructor(message) { + Object.assign(this, message); + this.time_sent = message.time_sent; + this.phase = message.phase; + this.sender = message.sender; + this.recipient = message.recipient; + this.message = message.message; + } + + isGlobal() { + return this.recipient === GLOBAL; + } + +} diff --git a/diplomacy/web/src/diplomacy/engine/power.js b/diplomacy/web/src/diplomacy/engine/power.js new file mode 100644 index 0000000..d13dcd4 --- /dev/null +++ b/diplomacy/web/src/diplomacy/engine/power.js @@ -0,0 +1,129 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/** Class Power. **/ +import {SortedDict} from "../utils/sorted_dict"; +import {STRINGS} from "../utils/strings"; + +export class Power { + constructor(name, role, game) { + this.game = game; + this.role = role; + + this.name = name; + this.controller = new SortedDict(); + this.vote = null; + this.order_is_set = 0; + this.wait = !this.game.isRealTime(); + this.centers = []; + this.homes = []; + this.units = []; + this.retreats = {}; + this.orders = []; + this.influence = []; + } + + isControlled() { + if (this.controller && this.controller.size()) { + return this.controller.lastValue() !== STRINGS.DUMMY; + } + return false; + } + + getController() { + return (this.controller && this.controller.lastValue()) || STRINGS.DUMMY; + } + + isEliminated() { + return !(this.units.length || this.centers.length || Object.keys(this.retreats).length); + } + + setState(powerState) { + this.name = powerState.name; + this.controller = new SortedDict(powerState.controller); + this.vote = powerState.vote; + this.order_is_set = powerState.order_is_set; + this.wait = powerState.wait; + this.centers = powerState.centers; + this.homes = powerState.homes; + this.units = powerState.units; + this.retreats = powerState.retreats; + this.influence = powerState.influence || []; + // Get orders. + this.orders = []; + if (this.game.phase.charAt(this.game.phase.length - 1) === 'M') { + if (this.game.isNoCheck()) { + for (let value of Object.values(powerState.orders)) if (value) + this.orders.push(value); + } else { + for (let unit of Object.keys(powerState.orders)) + this.orders.push(unit + ' ' + powerState.orders[unit]); + } + } else { + for (let order of powerState.adjust) + if (order && order !== 'WAIVE' && !order.startsWith('VOID ')) + this.orders.push(order); + } + } + + copy() { + const power = new Power(this.name, this.role, this.game); + for (let key of this.controller.keys()) + power.controller.put(key, this.controller.get(key)); + power.vote = this.vote; + power.order_is_set = this.order_is_set; + power.wait = this.wait; + power.centers = this.centers.slice(); + power.homes = this.homes.slice(); + power.units = this.units.slice(); + power.retreats = Object.assign({}, this.retreats); + power.influence = this.influence.slice(); + power.orders = this.orders.slice(); + return power; + } + + updateController(controller, timestamp) { + this.controller.put(timestamp, controller); + } + + setOrders(orders) { + this.orders = orders.slice(); + this.order_is_set = this.orders.length ? 2 : 1; + } + + setDummy() { + this.controller.clear(); + } + + clearCenters() { + this.centers = []; + } + + clearOrders() { + this.orders = []; + this.order_is_set = 0; + this.wait = !this.game.isRealTime(); + } + + clearUnits() { + this.units = []; + this.influence = []; + } + + getOrders() { + return this.orders.slice(); + } +} diff --git a/diplomacy/web/src/diplomacy/utils/diplog.js b/diplomacy/web/src/diplomacy/utils/diplog.js new file mode 100644 index 0000000..1e4a753 --- /dev/null +++ b/diplomacy/web/src/diplomacy/utils/diplog.js @@ -0,0 +1,45 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/*eslint no-console: ["error", {allow: ["log", "info", "warn", "error"]}] */ +export class Diplog { + static error(msg) { + console.error(msg); + } + + static warn(msg) { + console.warn(msg); + } + + static info(msg) { + console.info(msg); + } + + static success(msg) { + console.log(msg); + } + + static printMessages(messages) { + if (messages) { + if (messages.error) + Diplog.error(messages.error); + if (messages.info) + Diplog.info(messages.info); + if (messages.success) + Diplog.success(messages.success); + } + } +} diff --git a/diplomacy/web/src/diplomacy/utils/future.js b/diplomacy/web/src/diplomacy/utils/future.js new file mode 100644 index 0000000..c8d8add --- /dev/null +++ b/diplomacy/web/src/diplomacy/utils/future.js @@ -0,0 +1,55 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/** Class Future (like Python's Tornado future). **/ +export class Future { + constructor() { + this.__resolve_fn = null; + this.__reject_fn = null; + this.__promise = null; + this.__done = false; + + const future = this; + this.__promise = new Promise((resolve, reject) => { + future.__resolve_fn = resolve; + future.__reject_fn = reject; + }); + } + + promise() { + return this.__promise; + } + + setResult(result) { + if (!this.done()) { + this.__done = true; + const resolve_fn = this.__resolve_fn; + resolve_fn(result); + } + } + + setException(exception) { + if (!this.done()) { + this.__done = true; + const reject_fn = this.__reject_fn; + reject_fn(exception); + } + } + + done() { + return this.__done; + } +} diff --git a/diplomacy/web/src/diplomacy/utils/future_event.js b/diplomacy/web/src/diplomacy/utils/future_event.js new file mode 100644 index 0000000..a9dfcd8 --- /dev/null +++ b/diplomacy/web/src/diplomacy/utils/future_event.js @@ -0,0 +1,41 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {Future} from "./future"; + +/** Class FutureEvent (like Python's Tornado FutureEvent). **/ +export class FutureEvent { + constructor() { + this.__future = new Future(); + } + + set(error) { + if (!this.__future.done()) + if (error) + this.__future.setException(error); + else + this.__future.setResult(null); + } + + clear() { + if (this.__future.done()) + this.__future = new Future(); + } + + wait() { + return this.__future.promise(); + } +} diff --git a/diplomacy/web/src/diplomacy/utils/sorted_dict.js b/diplomacy/web/src/diplomacy/utils/sorted_dict.js new file mode 100644 index 0000000..6a27f00 --- /dev/null +++ b/diplomacy/web/src/diplomacy/utils/sorted_dict.js @@ -0,0 +1,109 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {UTILS} from "./utils"; + +function defaultComparableKey(key) { + return key; +} + +export class SortedDict { + constructor(dct, keyFn) { + this.__real_keys = []; + this.__keys = []; + this.__values = []; + this.__key_fn = keyFn || defaultComparableKey; + if (dct) for (let key of Object.keys(dct)) + this.put(key, dct[key]); + } + + clear() { + this.__real_keys = []; + this.__keys = []; + this.__values = []; + } + + put(key, value) { + const realKey = key; + key = this.__key_fn(key); + const position = UTILS.binarySearch.insert(this.__keys, key); + if (position === this.__values.length) { + this.__values.push(value); + this.__real_keys.push(realKey); + } else if (this.__values[position] !== value) { + this.__values.splice(position, 0, value); + this.__real_keys.splice(position, 0, realKey); + } + return position; + } + + remove(key) { + key = this.__key_fn(key); + const position = UTILS.binarySearch.find(this.__keys, key); + if (position < 0) + return null; + this.__keys.splice(position, 1); + this.__real_keys.splice(position, 1); + return this.__values.splice(position, 1)[0]; + } + + contains(key) { + return UTILS.binarySearch.find(this.__keys, this.__key_fn(key)) >= 0; + } + + get(key) { + const position = UTILS.binarySearch.find(this.__keys, this.__key_fn(key)); + if (position < 0) + return null; + return this.__values[position]; + } + + indexOf(key) { + return UTILS.binarySearch.find(this.__keys, this.__key_fn(key)); + } + + keyFromIndex(index) { + return this.__real_keys[index]; + } + + valueFromIndex(index) { + return this.__values[index]; + } + + size() { + return this.__keys.length; + } + + lastKey() { + if (!this.__keys.length) + throw new Error('Sorted dict is empty.'); + return this.__real_keys[this.__keys.length - 1]; + } + + lastValue() { + if (!this.__keys.length) + throw new Error('Sorted dict is empty.'); + return this.__values[this.__values.length - 1]; + } + + keys() { + return this.__real_keys.slice(); + } + + values() { + return this.__values.slice(); + } +} diff --git a/diplomacy/web/src/diplomacy/utils/strings.js b/diplomacy/web/src/diplomacy/utils/strings.js new file mode 100644 index 0000000..651e878 --- /dev/null +++ b/diplomacy/web/src/diplomacy/utils/strings.js @@ -0,0 +1,86 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/** Strings. **/ +export const STRINGS = { + ACTIVE: 'active', + ADMIN: 'admin', + CANCELED: 'canceled', + CHANNEL: 'channel', + COMPLETED: 'completed', + DEMOTE: 'demote', + DUMMY: 'dummy', + ERROR: 'error', + GAME: 'game', + MASTER_TYPE: 'master_type', + MODERATOR: 'moderator', + OBSERVER: 'observer', + OBSERVER_TYPE: 'observer_type', + OK: 'ok', + OMNISCIENT: 'omniscient', + OMNISCIENT_TYPE: 'omniscient_type', + PAUSED: 'paused', + PHASE: 'phase', + PROMOTE: 'promote', + STATE: 'state', + STATE_HISTORY: 'state_history', + SYNCHRONIZE: 'synchronize', + ALL_GAME_STATUSES: ['forming', 'active', 'paused', 'completed', 'canceled'], + ALL_POWER_NAMES: ['AUSTRIA', 'ENGLAND', 'FRANCE', 'GERMANY', 'ITALY', 'RUSSIA', 'TURKEY'], + RULES: [ + 'ALWAYS_WAIT', + 'BUILD_ANY', + 'CD_DUMMIES', + 'CIVIL_DISORDER', + 'DIFFERENT_ADJUDICATION', + 'DONT_SKIP_PHASES', + 'HOLD_WIN', + 'IGNORE_ERRORS', + 'MULTIPLE_POWERS_PER_PLAYER', + 'NO_CHECK', + 'NO_DEADLINE', + 'NO_DIAS', + 'NO_OBSERVATIONS', + 'NO_PRESS', + 'POWER_CHOICE', + 'PROPOSE_DIAS', + 'PUBLIC_PRESS', + 'REAL_TIME', + 'SHARED_VICTORY', + 'SOLITAIRE', + 'START_MASTER', + ], + PUBLIC_RULES: [ + 'ALWAYS_WAIT', + 'BUILD_ANY', + 'CD_DUMMIES', + 'CIVIL_DISORDER', + 'DONT_SKIP_PHASES', + 'HOLD_WIN', + 'IGNORE_ERRORS', + 'MULTIPLE_POWERS_PER_PLAYER', + 'NO_DEADLINE', + 'NO_DIAS', + 'NO_OBSERVATIONS', + 'NO_PRESS', + 'PROPOSE_DIAS', + 'PUBLIC_PRESS', + 'REAL_TIME', + 'SHARED_VICTORY', + 'SOLITAIRE', + 'START_MASTER', + ] +}; diff --git a/diplomacy/web/src/diplomacy/utils/utils.js b/diplomacy/web/src/diplomacy/utils/utils.js new file mode 100644 index 0000000..f398cd2 --- /dev/null +++ b/diplomacy/web/src/diplomacy/utils/utils.js @@ -0,0 +1,188 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/** Utils. **/ + +class Dict { +} + +export const UTILS = { + NB_CONNECTION_ATTEMPTS: 12, + ATTEMPT_DELAY_SECONDS: 5, + REQUEST_TIMEOUT_SECONDS: 30, + + /** Return a random integer in interval [from, to). **/ + randomInteger: function (from, to) { + return Math.floor(Math.random() * (to - from) + from); + }, + + /** Create an ID string using current time + 5 random integers each with 10 digits. **/ + createID: function () { + let id = new Date().getTime().toString(10); + for (let i = 0; i < 5; ++i) + id += UTILS.randomInteger(1e9, 1e10); + return id; + }, + + date: function () { + const d = new Date(); + return d.toLocaleString() + '.' + d.getMilliseconds(); + }, + + microsecondsToDate: function (time) { + return new Date(Math.floor(time / 1000)); + }, + + binarySearch: { + find: function (array, element) { + let a = 0; + let b = array.length - 1; + while (a <= b) { + const c = Math.floor((a + b) / 2); + if (array[c] === element) + return c; + if (array[c] < element) + a = c + 1; + else + b = c - 1; + } + return -1; + }, + insert: function (array, element) { + let a = 0; + let b = array.length - 1; + while (a <= b) { + const c = Math.floor((a + b) / 2); + if (array[c] === element) + return c; + if (array[c] < element) + a = c + 1; + else + b = c - 1; + } + // If we go out of loop, then array[b] < element, so we must insert element at position b + 1. + if (b < array.length - 1) + array.splice(b + 1, 0, element); + else + array.push(element); + return b + 1; + } + }, + + javascript: { + + arrayIsEmpty: function (array) { + return !(array && array.length); + }, + + hasArray: function (array) { + return array && array.length; + }, + + clearObject: function (obj) { + const keys = Object.keys(obj); + for (let key of keys) + delete obj[key]; + }, + + /** Create a dictionary from given array, using array elements as dictionary values + * and array elements's `field` values (element[field]) as dictionary keys. **/ + arrayToDict: function (array, field) { + const dictionary = {}; + for (let entry of array) + dictionary[entry[field]] = entry; + return dictionary; + }, + + count(obj) { + return Object.keys(obj).length; + }, + + extendArrayWithUniqueValues(obj, key, value) { + if (!obj.hasOwnProperty(key)) + obj[key] = [value]; + else if (!obj[key].includes(value)) + obj[key].push(value); + }, + + extendTreeValue: function (obj, path, value, allowMultipleValues) { + let current = obj; + const pathLength = path.length; + const parentPathLength = pathLength - 1; + for (let i = 0; i < parentPathLength; ++i) { + const stepName = path[i]; + if (!current.hasOwnProperty(stepName)) + current[stepName] = new Dict(); + current = current[stepName]; + } + const stepName = path[pathLength - 1]; + if (!current.hasOwnProperty(stepName)) + current[stepName] = []; + if (allowMultipleValues || !current[stepName].includes(value)) + current[stepName].push(value); + }, + + getTreeValue: function (obj, path) { + let current = obj; + for (let stepName of path) { + if (!current.hasOwnProperty(stepName)) + return null; + current = current[stepName]; + } + if (current instanceof Dict) + return Object.keys(current); + return current; + } + }, + + html: { + + // Source: https://www.w3schools.com/charsets/ref_utf_geometric.asp + UNICODE_LEFT_ARROW: '\u25C0', + UNICODE_RIGHT_ARROW: '\u25B6', + UNICODE_TOP_ARROW: '\u25BC', + UNICODE_BOTTOM_ARROW: '\u25B2', + CROSS: '\u00D7', + UNICODE_SMALL_RIGHT_ARROW: '\u2192', + UNICODE_SMALL_LEFT_ARROW: '\u2190', + + isSelect: function (element) { + return element.tagName.toLowerCase() === 'select'; + }, + + isInput: function (element) { + return element.tagName.toLowerCase() === 'input'; + }, + + isCheckBox: function (element) { + return UTILS.html.isInput(element) && element.type === 'checkbox'; + }, + + isRadioButton: function (element) { + return UTILS.html.isInput(element) && element.type === 'radio'; + }, + + isTextInput: function (element) { + return UTILS.html.isInput(element) && element.type === 'text'; + }, + + isPasswordInput: function (element) { + return UTILS.html.isInput(element) && element.type === 'password'; + } + + } + +}; diff --git a/diplomacy/web/src/gui/core/content.jsx b/diplomacy/web/src/gui/core/content.jsx new file mode 100644 index 0000000..416ba9e --- /dev/null +++ b/diplomacy/web/src/gui/core/content.jsx @@ -0,0 +1,51 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import PropTypes from 'prop-types'; + +export class Content extends React.Component { + // PROPERTIES: + // page: pointer to parent Page object + // data: data for current content + + // Each derived class must implement this static method. + static builder(page, data) { + return { + // page title (string) + title: `${data ? 'with data' : 'without data'}`, + // page navigation links: array of couples + // (navigation title, navigation callback ( onClick=() => callback() )) + navigation: [], + // page content: React component (e.g. <MyComponent/>, or <div class="content">...</div>, etc). + component: null + }; + } + + getPage() { + return this.props.page; + } + + componentDidMount() { + window.scrollTo(0, 0); + } +} + + +Content.propTypes = { + page: PropTypes.object.isRequired, + data: PropTypes.object +}; diff --git a/diplomacy/web/src/gui/core/fancybox.jsx b/diplomacy/web/src/gui/core/fancybox.jsx new file mode 100644 index 0000000..4d1013d --- /dev/null +++ b/diplomacy/web/src/gui/core/fancybox.jsx @@ -0,0 +1,59 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import {Button} from "./widgets"; +import PropTypes from 'prop-types'; + +const TIMES = '\u00D7'; + +export class FancyBox extends React.Component { + // open-tag (<FancyBox></FancyBox>) + // PROPERTIES + // title + // onClose + render() { + return ( + <div className={'fancy-wrapper'} onClick={this.props.onClose}> + <div className={'fancy-box container'} onClick={(event) => { + if (!event) + event = window.event; + if (event.hasOwnProperty('cancelBubble')) + event.cancelBubble = true; + if (event.stopPropagation) + event.stopPropagation(); + }}> + <div className={'row fancy-bar'}> + <div className={'col-11 align-self-center fancy-title'}>{this.props.title}</div> + <div className={'col-1 fancy-button'}> + <Button title={TIMES} color={'danger'} onClick={this.props.onClose}/> + </div> + </div> + <div className={'row'}> + <div className={'col fancy-content'}>{this.props.children}</div> + </div> + </div> + </div> + ); + } +} + + +FancyBox.propTypes = { + title: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) +}; diff --git a/diplomacy/web/src/gui/core/forms.jsx b/diplomacy/web/src/gui/core/forms.jsx new file mode 100644 index 0000000..76d188c --- /dev/null +++ b/diplomacy/web/src/gui/core/forms.jsx @@ -0,0 +1,116 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import {Button} from "./widgets"; +import {UTILS} from "../../diplomacy/utils/utils"; + +export class Forms { + static createOnChangeCallback(component, callback) { + return (event) => { + const value = UTILS.html.isCheckBox(event.target) ? event.target.checked : event.target.value; + const fieldName = UTILS.html.isRadioButton(event.target) ? event.target.name : event.target.id; + const update = {[fieldName]: value}; + const state = Object.assign({}, component.state, update); + if (callback) + callback(state); + component.setState(state); + }; + } + + static createOnSubmitCallback(component, callback, resetState) { + return (event) => { + if (callback) + callback(Object.assign({}, component.state)); + if (resetState) + component.setState(resetState); + event.preventDefault(); + }; + } + + static createOnResetCallback(component, onChangeCallback, resetState) { + return (event) => { + if (onChangeCallback) + onChangeCallback(resetState); + component.setState(resetState); + if (event && event.preventDefault) + event.preventDefault(); + }; + } + + static getValue(fieldValues, fieldName, defaultValue) { + return fieldValues.hasOwnProperty(fieldName) ? fieldValues[fieldName] : defaultValue; + } + + static createReset(title, large, onReset) { + return <Button key={'reset'} title={title || 'reset'} onClick={onReset} pickEvent={true} large={large}/>; + } + + static createSubmit(title, large, onSubmit) { + return <Button key={'submit'} title={title || 'submit'} onClick={onSubmit} pickEvent={true} large={large}/>; + } + + static createButton(title, fn, color, large) { + const wrapFn = (event) => { + fn(); + event.preventDefault(); + }; + return <Button large={large} key={title} color={color} title={title} onClick={wrapFn} pickEvent={true}/>; + } + + static createCheckbox(id, title, value, onChange) { + const input = <input className={'form-check-input'} key={id} type={'checkbox'} id={id} checked={value} + onChange={onChange}/>; + const label = <label className={'form-check-label'} key={`label-${id}`} htmlFor={id}>{title}</label>; + return [input, label]; + } + + static createRadio(name, value, title, currentValue, onChange) { + const id = `[${name}][${value}]`; + const input = <input className={'form-check-input'} key={id} type={'radio'} + name={name} value={value} checked={currentValue === value} + id={id} onChange={onChange}/>; + const label = <label className={'form-check-label'} key={`label-${id}`} htmlFor={id}>{title || value}</label>; + return [input, label]; + } + + static createRow(label, input) { + return ( + <div className={'form-group row'}> + {label} + <div className={'col'}>{input}</div> + </div> + ); + } + + static createLabel(htmFor, title, className) { + return <label className={className} htmlFor={htmFor}>{title}</label>; + } + + static createColLabel(htmlFor, title) { + return Forms.createLabel(htmlFor, title, 'col'); + } + + static createSelectOptions(values, none) { + const options = values.slice(); + const components = options.map((option, index) => <option key={index} value={option}>{option}</option>); + if (none) { + components.splice(0, 0, [<option key={-1} value={''}>{none === true ? '(none)' : `${none}`}</option>]); + } + return components; + } +} + diff --git a/diplomacy/web/src/gui/core/layouts.jsx b/diplomacy/web/src/gui/core/layouts.jsx new file mode 100644 index 0000000..78189e4 --- /dev/null +++ b/diplomacy/web/src/gui/core/layouts.jsx @@ -0,0 +1,55 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import PropTypes from 'prop-types'; + +class Div extends React.Component { + getClassName() { + return ''; + } + + render() { + return ( + <div className={this.getClassName() + (this.props.className ? ' ' + this.props.className : '')}> + {this.props.children} + </div> + ); + } +} + +Div.propTypes = { + className: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) +}; + +export class Bar extends Div { + getClassName() { + return 'bar'; + } +} + +export class Row extends Div { + getClassName() { + return 'row'; + } +} + +export class Col extends Div { + getClassName() { + return 'col'; + } +} diff --git a/diplomacy/web/src/gui/core/page.jsx b/diplomacy/web/src/gui/core/page.jsx new file mode 100644 index 0000000..5ca09fd --- /dev/null +++ b/diplomacy/web/src/gui/core/page.jsx @@ -0,0 +1,434 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/** Main class to use to create app GUI. **/ + +import React from "react"; +import {ContentConnection} from "../diplomacy/contents/content_connection"; +import {ContentGames} from "../diplomacy/contents/content_games"; +import {ContentGame} from "../diplomacy/contents/content_game"; +import {UTILS} from "../../diplomacy/utils/utils"; +import {Diplog} from "../../diplomacy/utils/diplog"; +import {STRINGS} from "../../diplomacy/utils/strings"; +import {Game} from "../../diplomacy/engine/game"; +import Octicon, {Person} from '@githubprimer/octicons-react'; +import $ from "jquery"; +import {FancyBox} from "./fancybox"; +import {DipStorage} from "../diplomacy/utils/dipStorage"; + +const CONTENTS = { + connection: ContentConnection, + games: ContentGames, + game: ContentGame +}; + +export class Page extends React.Component { + + constructor(props) { + super(props); + this.connection = null; + this.channel = null; + this.availableMaps = null; + this.state = { + // fancybox, + fancyTitle: null, + onFancyBox: null, + // Page messages + error: null, + info: null, + success: null, + title: null, + // Page content parameters + contentName: 'connection', + contentData: null, + // Games. + games: {}, // Games found. + myGames: {} // Games locally stored. + }; + this.loadPage = this.loadPage.bind(this); + this.loadConnection = this.loadConnection.bind(this); + this.loadGames = this.loadGames.bind(this); + this.loadGame = this.loadGame.bind(this); + this.loadGameFromDisk = this.loadGameFromDisk.bind(this); + this.logout = this.logout.bind(this); + this.error = this.error.bind(this); + this.info = this.info.bind(this); + this.success = this.success.bind(this); + this.unloadFancyBox = this.unloadFancyBox.bind(this); + } + + static wrapMessage(message) { + return message ? `(${UTILS.date()}) ${message}` : ''; + } + + static __sort_games(games) { + // Sort games with not-joined games first, else compare game ID. + games.sort((a, b) => (((a.role ? 1 : 0) - (b.role ? 1 : 0)) || a.game_id.localeCompare(b.game_id))); + return games; + } + + copyState(updatedFields) { + return Object.assign({}, this.state, updatedFields || {}); + } + + //// Methods to check page type. + + __page_is(contentName, contentData) { + return this.state.contentName === contentName && (!contentData || this.state.contentData === contentData); + } + + pageIsConnection(contentData) { + return this.__page_is('connection', contentData); + } + + pageIsGames(contentData) { + return this.__page_is('games', contentData); + } + + pageIsGame(contentData) { + return this.__page_is('game', contentData); + } + + //// Methods to load a global fancybox. + + loadFancyBox(title, callback) { + this.setState({fancyTitle: title, onFancyBox: callback}); + } + + unloadFancyBox() { + this.setState({fancyTitle: null, onFancyBox: null}); + } + + //// Methods to load a page. + + loadPage(contentName, contentData, messages) { + messages = messages || {}; + messages.error = Page.wrapMessage(messages.error); + messages.info = Page.wrapMessage(messages.info); + messages.success = Page.wrapMessage(messages.success); + Diplog.printMessages(messages); + this.setState(this.copyState({ + error: messages.error, + info: messages.info, + success: messages.success, + contentName: contentName, + contentData: contentData, + title: null, + fancyTitle: null, + onFancyBox: null + })); + } + + loadConnection(contentData, messages) { + this.loadPage('connection', contentData, messages); + } + + loadGames(contentData, messages) { + this.loadPage('games', contentData, messages); + } + + loadGame(gameInfo, messages) { + this.loadPage('game', gameInfo, messages); + } + + loadGameFromDisk() { + const input = $(document.createElement('input')); + input.attr("type", "file"); + input.trigger('click'); + input.change(event => { + const file = event.target.files[0]; + if (!file.name.match(/\.json$/i)) { + this.error(`Invalid JSON filename ${file.name}`); + } else { + const reader = new FileReader(); + reader.onload = () => { + const savedData = JSON.parse(reader.result); + const gameObject = {}; + gameObject.game_id = `(local) ${savedData.id}`; + gameObject.map_name = savedData.map; + gameObject.rules = savedData.rules; + const state_history = {}; + const message_history = {}; + const order_history = {}; + const result_history = {}; + for (let savedPhase of savedData.phases) { + const gameState = savedPhase.state; + const phaseOrders = savedPhase.orders || {}; + const phaseResults = savedPhase.results || {}; + const phaseMessages = {}; + if (savedPhase.messages) { + for (let message of savedPhase.messages) { + phaseMessages[message.time_sent] = message; + } + } + if (!gameState.name) + gameState.name = savedPhase.name; + state_history[gameState.name] = gameState; + order_history[gameState.name] = phaseOrders; + message_history[gameState.name] = phaseMessages; + result_history[gameState.name] = phaseResults; + } + gameObject.state_history = state_history; + gameObject.message_history = message_history; + gameObject.order_history = order_history; + gameObject.state_history = state_history; + gameObject.result_history = result_history; + gameObject.messages = []; + gameObject.role = STRINGS.OBSERVER_TYPE; + gameObject.status = STRINGS.COMPLETED; + gameObject.timestamp_created = 0; + gameObject.deadline = 0; + gameObject.n_controls = 0; + gameObject.registration_password = ''; + const game = new Game(gameObject); + this.loadGame(game); + }; + reader.readAsText(file); + } + }); + } + + //// Methods to sign out channel and go back to connection page. + + __disconnect() { + // Clear local data and go back to connection page. + this.connection.close(); + this.connection = null; + this.channel = null; + this.availableMaps = null; + const message = Page.wrapMessage(`Disconnected from channel and server.`); + Diplog.success(message); + this.setState(this.copyState({ + error: null, + info: null, + success: message, + contentName: 'connection', + contentData: null, + // When disconnected, remove all games previously loaded. + games: {}, + myGames: {} + })); + } + + logout() { + // Disconnect channel and go back to connection page. + if (this.channel) { + this.channel.logout() + .then(() => this.__disconnect()) + .catch(error => this.error(`Error while disconnecting: ${error.toString()}.`)); + } else { + this.__disconnect(); + } + } + + //// Methods to be used to set page title and messages. + + setTitle(title) { + this.setState({title: title}); + } + + error(message) { + message = Page.wrapMessage(message); + Diplog.error(message); + this.setState({error: message}); + } + + info(message) { + message = Page.wrapMessage(message); + Diplog.info(message); + this.setState({info: message}); + } + + success(message) { + message = Page.wrapMessage(message); + Diplog.success(message); + this.setState({success: message}); + } + + warn(message) { + this.info(message); + } + + //// Methods to manage games. + + updateMyGames(gamesToAdd) { + // Update state myGames with given games. This method does not update local storage. + const myGames = Object.assign({}, this.state.myGames); + let gamesFound = null; + for (let gameToAdd of gamesToAdd) { + myGames[gameToAdd.game_id] = gameToAdd; + if (this.state.games.hasOwnProperty(gameToAdd.game_id)) { + if (!gamesFound) + gamesFound = Object.assign({}, this.state.games); + gamesFound[gameToAdd.game_id] = gameToAdd; + } + } + if (!gamesFound) + gamesFound = this.state.games; + this.setState({myGames: myGames, games: gamesFound}); + } + + getMyGames() { + return Page.__sort_games(Object.values(this.state.myGames)); + } + + getGamesFound() { + return Page.__sort_games(Object.values(this.state.games)); + } + + addGamesFound(gamesToAdd) { + const gamesFound = {}; + for (let game of gamesToAdd) { + gamesFound[game.game_id] = ( + this.state.myGames.hasOwnProperty(game.game_id) ? + this.state.myGames[game.game_id] : game + ); + } + this.setState({games: gamesFound}); + } + + leaveGame(gameID) { + if (this.state.myGames.hasOwnProperty(gameID)) { + const game = this.state.myGames[gameID]; + if (game.client) { + game.client.leave() + .then(() => { + this.disconnectGame(gameID); + this.loadGames(null, {info: `Game ${gameID} left.`}); + }) + .catch(error => this.error(`Error when leaving game ${gameID}: ${error.toString()}`)); + } + } + } + + disconnectGame(gameID) { + if (this.state.myGames.hasOwnProperty(gameID)) { + const game = this.state.myGames[gameID]; + if (game.client) + game.client.clearAllCallbacks(); + this.channel.getGamesInfo({games: [gameID]}) + .then(gamesInfo => { + this.updateMyGames(gamesInfo); + }) + .catch(error => this.error(`Error while leaving game ${gameID}: ${error.toString()}`)); + } + } + + addToMyGames(game) { + // Update state myGames with given game **and** update local storage. + const myGames = Object.assign({}, this.state.myGames); + const gamesFound = this.state.games.hasOwnProperty(game.game_id) ? Object.assign({}, this.state.games) : this.state.games; + myGames[game.game_id] = game; + if (gamesFound.hasOwnProperty(game.game_id)) + gamesFound[game.game_id] = game; + DipStorage.addUserGame(this.channel.username, game.game_id); + this.setState({myGames: myGames, games: gamesFound}); + } + + removeFromMyGames(gameID) { + if (this.state.myGames.hasOwnProperty(gameID)) { + const games = Object.assign({}, this.state.myGames); + delete games[gameID]; + DipStorage.removeUserGame(this.channel.username, gameID); + this.setState({myGames: games}); + } + } + + hasMyGame(gameID) { + return this.state.myGames.hasOwnProperty(gameID); + } + + //// Render method. + + render() { + const content = CONTENTS[this.state.contentName].builder(this, this.state.contentData); + const hasNavigation = UTILS.javascript.hasArray(content.navigation); + + // NB: I currently don't find a better way to update document title from content details. + const successMessage = this.state.success || '-'; + const infoMessage = this.state.info || '-'; + const errorMessage = this.state.error || '-'; + const title = this.state.title || content.title; + document.title = title + ' | Diplomacy'; + + return ( + <div className="page container-fluid" id={this.state.contentName}> + <div className={'top-msg row'}> + <div title={successMessage !== '-' ? successMessage : ''} + className={'col-sm-4 msg success ' + (this.state.success ? 'with-msg' : 'no-msg')} + onClick={() => this.success()}> + {successMessage} + </div> + <div title={infoMessage !== '-' ? infoMessage : ''} + className={'col-sm-4 msg info ' + (this.state.info ? 'with-msg' : 'no-msg')} + onClick={() => this.info()}> + {infoMessage} + </div> + <div title={errorMessage !== '-' ? errorMessage : ''} + className={'col-sm-4 msg error ' + (this.state.error ? 'with-msg' : 'no-msg')} + onClick={() => this.error()}> + {errorMessage} + </div> + </div> + {((hasNavigation || this.channel) && ( + <div className={'title row'}> + <div className={'col align-self-center'}><strong>{title}</strong></div> + <div className={'col-sm-1'}> + {(!hasNavigation && ( + <div className={'float-right'}> + <strong> + <u className={'mr-2'}>{this.channel.username}</u> + <Octicon icon={Person}/> + </strong> + </div> + )) || ( + <div className="dropdown float-right"> + <button className="btn btn-secondary dropdown-toggle" type="button" + id="dropdownMenuButton" data-toggle="dropdown" + aria-haspopup="true" aria-expanded="false"> + {(this.channel && this.channel.username && ( + <span> + <u className={'mr-2'}>{this.channel.username}</u> + <Octicon icon={Person}/> + </span> + )) || 'Menu'} + </button> + <div className="dropdown-menu dropdown-menu-right" + aria-labelledby="dropdownMenuButton"> + {content.navigation.map((nav, index) => { + const navTitle = nav[0]; + const navAction = nav[1]; + return <a key={index} className="dropdown-item" + onClick={navAction}>{navTitle}</a>; + })} + </div> + </div> + )} + </div> + </div> + )) || ( + <div className={'title'}><strong>{title}</strong></div> + )} + {content.component} + {this.state.onFancyBox && ( + <FancyBox title={this.state.fancyTitle} onClose={this.unloadFancyBox}> + {this.state.onFancyBox()} + </FancyBox> + )} + </div> + ); + } +} diff --git a/diplomacy/web/src/gui/core/table.jsx b/diplomacy/web/src/gui/core/table.jsx new file mode 100644 index 0000000..cb729e7 --- /dev/null +++ b/diplomacy/web/src/gui/core/table.jsx @@ -0,0 +1,112 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +//// Tables. + +import React from "react"; +import PropTypes from 'prop-types'; + +class DefaultWrapper { + constructor(data) { + this.data = data; + this.get = this.get.bind(this); + } + + get(fieldName) { + return this.data[fieldName]; + } +} + +function defaultWrapper(data) { + return new DefaultWrapper(data); +} + +export class Table extends React.Component { + // className + // caption + // columns : {name: [title, order]} + // data: [objects with expected column names] + // wrapper: (optional) function to use to wrap one data entry into an object before accessing fields. + // Must return an instance with a method get(name). + // If provided: wrapper(data_entry).get(field_name) + // else: data_entry[field_name] + + constructor(props) { + super(props); + if (!this.props.wrapper) + this.props.wrapper = defaultWrapper; + } + + static getHeader(columns) { + const header = []; + for (let entry of Object.entries(columns)) { + const name = entry[0]; + const title = entry[1][0]; + const order = entry[1][1]; + header.push([order, name, title]); + } + header.sort((a, b) => { + let t = a[0] - b[0]; + if (t === 0) + t = a[1].localeCompare(b[1]); + if (t === 0) + t = a[2].localeCompare(b[2]); + return t; + }); + return header; + } + + static getHeaderLine(header) { + return ( + <thead className={'thead-light'}> + <tr>{header.map((column, colIndex) => <th key={colIndex}>{column[2]}</th>)}</tr> + </thead> + ); + } + + static getBodyRow(header, row, rowIndex, wrapper) { + const wrapped = wrapper(row); + return (<tr key={rowIndex}> + {header.map((headerColumn, colIndex) => <td className={'align-middle'} + key={colIndex}>{wrapped.get(headerColumn[1])}</td>)} + </tr>); + } + + static getBodyLines(header, data, wrapper) { + return (<tbody>{data.map((row, rowIndex) => Table.getBodyRow(header, row, rowIndex, wrapper))}</tbody>); + } + + render() { + const header = Table.getHeader(this.props.columns); + return ( + <div className={'table-responsive'}> + <table className={this.props.className}> + <caption>{this.props.caption} ({this.props.data.length})</caption> + {Table.getHeaderLine(header)} + {Table.getBodyLines(header, this.props.data, this.props.wrapper)} + </table> + </div> + ); + } +} + +Table.propTypes = { + wrapper: PropTypes.func, + columns: PropTypes.object, + className: PropTypes.string, + caption: PropTypes.string, + data: PropTypes.array +}; diff --git a/diplomacy/web/src/gui/core/tabs.jsx b/diplomacy/web/src/gui/core/tabs.jsx new file mode 100644 index 0000000..6123219 --- /dev/null +++ b/diplomacy/web/src/gui/core/tabs.jsx @@ -0,0 +1,96 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import {Action} from "./widgets"; +import PropTypes from 'prop-types'; + +export class Tab extends React.Component { + render() { + const style = { + display: this.props.display ? 'block' : 'none' + }; + const id = this.props.id ? {id: this.props.id} : {}; + return ( + <div className={'tab mb-4 ' + this.props.className} style={style} {...id}> + {this.props.children} + </div> + ); + } +} + +Tab.propTypes = { + display: PropTypes.bool, + className: PropTypes.string, + id: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) +}; + +Tab.defaultProps = { + display: false, + className: '', + id: '' +}; + +export class Tabs extends React.Component { + /** PROPERTIES + * active: index of active menu (must be > menu.length). + * highlights: dictionary mapping a menu indice to a highlight message + * onChange: callback(index): receive index of menu to display. + * **/ + + generateTabAction(tabTitle, tabId, isActive, onChange, highlight) { + return <Action isActive={isActive} + title={tabTitle} + onClick={() => onChange(tabId)} + highlight={highlight} + key={tabId}/>; + } + + render() { + if (!this.props.menu.length) + throw new Error(`No tab menu given.`); + if (this.props.menu.length !== this.props.titles.length) + throw new Error(`Menu length (${this.props.menu.length}) != titles length (${this.props.titles.length})`); + if (this.props.active && !this.props.menu.includes(this.props.active)) + throw new Error(`Invalid active tab name, got ${this.props.active}, expected one of: ${this.props.menu.join(', ')}`); + const active = this.props.active || this.props.menu[0]; + return ( + <div className={'tabs mb-3'}> + <nav className={'tabs-bar nav nav-tabs justify-content-center mb-3'}> + {this.props.menu.map((tabName, index) => this.generateTabAction( + this.props.titles[index], tabName, active === tabName, this.props.onChange, + (this.props.highlights.hasOwnProperty(tabName) && this.props.highlights[tabName]) || null + ))} + </nav> + {this.props.children} + </div> + ); + } +} + +Tabs.propTypes = { + menu: PropTypes.arrayOf(PropTypes.string).isRequired, // tab names + titles: PropTypes.arrayOf(PropTypes.string).isRequired, // tab titles + onChange: PropTypes.func.isRequired, // callback(tab name) + children: PropTypes.array.isRequired, + active: PropTypes.string, // current active tab name + highlights: PropTypes.object, // {tab name => highligh message (optional)} +}; + +Tabs.defaultProps = { + highlights: {} +}; diff --git a/diplomacy/web/src/gui/core/widgets.jsx b/diplomacy/web/src/gui/core/widgets.jsx new file mode 100644 index 0000000..62a5eb4 --- /dev/null +++ b/diplomacy/web/src/gui/core/widgets.jsx @@ -0,0 +1,102 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import PropTypes from 'prop-types'; + +export class Button extends React.Component { + /** Bootstrap button. + * Bootstrap classes: + * - btn + * - btn-primary + * - mx-1 (margin-left 1px, margin-right 1px) + * Props: title (str), onClick (function). + * **/ + // title + // onClick + // pickEvent = false + // large = false + // small = false + + constructor(props) { + super(props); + this.onClick = this.onClick.bind(this); + } + + onClick(event) { + if (this.props.onClick) + this.props.onClick(this.props.pickEvent ? event : null); + } + + render() { + return ( + <button + className={`btn btn-${this.props.color || 'secondary'}` + (this.props.large ? ' btn-block' : '') + (this.props.small ? ' btn-sm' : '')} + disabled={this.props.disabled} + onClick={this.onClick}> + <strong>{this.props.title}</strong> + </button> + ); + } +} + +Button.propTypes = { + title: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + color: PropTypes.string, + large: PropTypes.bool, + small: PropTypes.bool, + pickEvent: PropTypes.bool, + disabled: PropTypes.bool +}; + +Button.defaultPropTypes = { + disabled: false +}; + + +export class Action extends React.Component { + // title + // isActive + // onClick + // See Button parameters. + + render() { + return ( + <div className="action nav-item" onClick={this.props.onClick}> + <div + className={'nav-link' + (this.props.isActive ? ' active' : '') + (this.props.highlight !== null ? ' updated' : '')}> + {this.props.title} + {this.props.highlight !== null + && this.props.highlight !== undefined + && <span className={'update'}>{this.props.highlight}</span>} + </div> + </div> + ); + } +} + +Action.propTypes = { + title: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + highlight: PropTypes.any, + isActive: PropTypes.bool +}; + +Action.defaultProps = { + highlight: null, + isActive: false +}; diff --git a/diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx b/diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx new file mode 100644 index 0000000..8aa7fb1 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx @@ -0,0 +1,91 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import {Content} from "../../core/content"; +import {Connection} from "../../../diplomacy/client/connection"; +import {ConnectionForm} from "../forms/connection_form"; +import {DipStorage} from "../utils/dipStorage"; + +export class ContentConnection extends Content { + constructor(props) { + super(props); + this.connection = null; + this.onSubmit = this.onSubmit.bind(this); + } + + static builder(page, data) { + return { + title: 'Connection', + navigation: [], + component: <ContentConnection page={page} data={data}/> + }; + } + + onSubmit(data) { + const page = this.getPage(); + for (let fieldName of ['hostname', 'port', 'username', 'password', 'showServerFields']) + if (!data.hasOwnProperty(fieldName)) + return page.error(`Missing ${fieldName}, got ${JSON.stringify(data)}`); + page.info('Connecting ...'); + if (this.connection) { + this.connection.currentConnectionProcessing.stop(); + } + this.connection = new Connection(data.hostname, data.port, window.location.protocol.toLowerCase() === 'https:'); + // Page is passed as logger object (with methods info(), error(), success()) when connecting. + this.connection.connect(this.getPage()) + .then(() => { + page.connection = this.connection; + this.connection = null; + page.success(`Successfully connected to server ${data.username}:${data.port}`); + page.connection.authenticate(data.username, data.password, false) + .catch((error) => { + page.error(`Unable to sign in, trying to create an account, error: ${error}`); + return page.connection.authenticate(data.username, data.password, true); + }) + .then((channel) => { + page.channel = channel; + return channel.getAvailableMaps(); + }) + .then(availableMaps => { + page.availableMaps = availableMaps; + const userGameIndices = DipStorage.getUserGames(page.channel.username); + if (userGameIndices && userGameIndices.length) { + return page.channel.getGamesInfo({games: userGameIndices}); + } else { + return null; + } + }) + .then((gamesInfo) => { + if (gamesInfo) { + this.getPage().success('Found ' + gamesInfo.length + ' user games.'); + this.getPage().updateMyGames(gamesInfo); + } + page.loadGames(null, {success: `Account ${data.username} connected.`}); + }) + .catch((error) => { + page.error('Error while authenticating: ' + error + ' Please re-try.'); + }); + }) + .catch((error) => { + page.error('Error while connecting: ' + error + ' Please re-try.'); + }); + } + + render() { + return <main><ConnectionForm onSubmit={this.onSubmit}/></main>; + } +} diff --git a/diplomacy/web/src/gui/diplomacy/contents/content_game.jsx b/diplomacy/web/src/gui/diplomacy/contents/content_game.jsx new file mode 100644 index 0000000..81a689d --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/contents/content_game.jsx @@ -0,0 +1,1235 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import Scrollchor from 'react-scrollchor'; +import {SelectLocationForm} from "../forms/select_location_form"; +import {SelectViaForm} from "../forms/select_via_form"; +import {Order} from "../utils/order"; +import {Button} from "../../core/widgets"; +import {Bar, Row} from "../../core/layouts"; +import {Content} from "../../core/content"; +import {Tab, Tabs} from "../../core/tabs"; +import {Map} from "../map/map"; +import {extendOrderBuilding, ORDER_BUILDER, POSSIBLE_ORDERS} from "../utils/order_building"; +import {PowerActionsForm} from "../forms/power_actions_form"; +import {MessageForm} from "../forms/message_form"; +import {UTILS} from "../../../diplomacy/utils/utils"; +import {Message} from "../../../diplomacy/engine/message"; +import {PowerOrder} from "../widgets/power_order"; +import {MessageView} from "../widgets/message_view"; +import {STRINGS} from "../../../diplomacy/utils/strings"; +import {Diplog} from "../../../diplomacy/utils/diplog"; +import {Table} from "../../core/table"; +import {PowerView} from "../utils/power_view"; +import {FancyBox} from "../../core/fancybox"; +import {DipStorage} from "../utils/dipStorage"; + +const HotKey = require('react-shortcut'); + +/* Order management in game page. + * When editing orders locally, we have to compare it to server orders + * to determine when we need to update orders on server side. There are + * 9 comparison cases, depending on orders: + * SERVER LOCAL DECISION + * null null 0 (same) + * null {} 1 (different, user wants to send "no orders" on server) + * null {orders} 1 (different, user defines new orders locally) + * {} null 0 (assumed same: user is not allowed to "delete" a "no orders": he can only add new orders) + * {} {} 0 (same) + * {} {orders} 1 (different, user defines new orders locally and wants to overwrite the "no-orders" on server) + * {orders} null 1 (different, user wants to delete all server orders, will result to "no-orders") + * {orders} {} 1 (different, user wants to delete all server orders, will result to "no-orders") + * {orders} {orders} same if we have exactly same orders on both server and local + * */ + +const TABLE_POWER_VIEW = { + name: ['Power', 0], + controller: ['Controller', 1], + order_is_set: ['With orders', 2], + wait: ['Waiting', 3] +}; + +function Help() { + return ( + <div> + <p>When building an order, press <strong>ESC</strong> to reset build.</p> + <p>Press letter associated to an order type to start building an order of this type. + <br/> Order type letter is indicated in order type name after order type radio button. + </p> + <p>In Phase History tab, use keyboard left and right arrows to navigate in past phases.</p> + </div> + ); +} + +export class ContentGame extends Content { + + constructor(props) { + super(props); + // Load local orders from local storage (if available). + const savedOrders = this.props.data.client ? DipStorage.getUserGameOrders( + this.props.data.client.channel.username, + this.props.data.game_id, + this.props.data.phase + ) : null; + let orders = null; + if (savedOrders) { + orders = {}; + for (let entry of Object.entries(savedOrders)) { + let powerOrders = null; + const powerName = entry[0]; + if (entry[1]) { + powerOrders = {}; + for (let orderString of entry[1]) { + const order = new Order(orderString, true); + powerOrders[order.loc] = order; + } + } + orders[powerName] = powerOrders; + } + } + this.schedule_timeout_id = null; + this.state = { + tabMain: null, + tabPastMessages: null, + tabCurrentMessages: null, + messageHighlights: {}, + historyPhaseIndex: null, + historyShowOrders: true, + historySubView: 0, + historyCurrentLoc: null, + historyCurrentOrders: null, + wait: null, // {power name => bool} + orders: orders, // {power name => {loc => {local: bool, order: str}}} + power: null, + orderBuildingType: null, + orderBuildingPath: [], + fancy_title: null, + fancy_function: null, + on_fancy_close: null, + }; + + // Bind some class methods to this instance. + this.closeFancyBox = this.closeFancyBox.bind(this); + this.displayFirstPastPhase = this.displayFirstPastPhase.bind(this); + this.displayLastPastPhase = this.displayLastPastPhase.bind(this); + this.displayLocationOrders = this.displayLocationOrders.bind(this); + this.getMapInfo = this.getMapInfo.bind(this); + this.notifiedGamePhaseUpdated = this.notifiedGamePhaseUpdated.bind(this); + this.notifiedLocalStateChange = this.notifiedLocalStateChange.bind(this); + this.notifiedNetworkGame = this.notifiedNetworkGame.bind(this); + this.notifiedNewGameMessage = this.notifiedNewGameMessage.bind(this); + this.notifiedPowersControllers = this.notifiedPowersControllers.bind(this); + this.onChangeCurrentPower = this.onChangeCurrentPower.bind(this); + this.onChangeMainTab = this.onChangeMainTab.bind(this); + this.onChangeOrderType = this.onChangeOrderType.bind(this); + this.onChangePastPhase = this.onChangePastPhase.bind(this); + this.onChangePastPhaseIndex = this.onChangePastPhaseIndex.bind(this); + this.onChangeShowPastOrders = this.onChangeShowPastOrders.bind(this); + this.onChangeTabCurrentMessages = this.onChangeTabCurrentMessages.bind(this); + this.onChangeTabPastMessages = this.onChangeTabPastMessages.bind(this); + this.onClickMessage = this.onClickMessage.bind(this); + this.onDecrementPastPhase = this.onDecrementPastPhase.bind(this); + this.onIncrementPastPhase = this.onIncrementPastPhase.bind(this); + this.onOrderBuilding = this.onOrderBuilding.bind(this); + this.onOrderBuilt = this.onOrderBuilt.bind(this); + this.onProcessGame = this.onProcessGame.bind(this); + this.onRemoveAllOrders = this.onRemoveAllOrders.bind(this); + this.onRemoveOrder = this.onRemoveOrder.bind(this); + this.onSelectLocation = this.onSelectLocation.bind(this); + this.onSelectVia = this.onSelectVia.bind(this); + this.onSetNoOrders = this.onSetNoOrders.bind(this); + this.reloadServerOrders = this.reloadServerOrders.bind(this); + this.renderOrders = this.renderOrders.bind(this); + this.sendMessage = this.sendMessage.bind(this); + this.setOrders = this.setOrders.bind(this); + this.setSelectedLocation = this.setSelectedLocation.bind(this); + this.setSelectedVia = this.setSelectedVia.bind(this); + this.setWaitFlag = this.setWaitFlag.bind(this); + this.vote = this.vote.bind(this); + } + + static gameTitle(game) { + let title = `${game.game_id} | ${game.phase} | ${game.status} | ${game.role} | ${game.map_name}`; + const remainingTime = game.deadline_timer; + if (remainingTime === undefined) + title += ` (deadline: ${game.deadline} sec)`; + else if (remainingTime) + title += ` (remaining ${remainingTime} sec)`; + return title; + } + + static saveGameToDisk(game, page) { + if (game.client) { + game.client.save() + .then((savedData) => { + const domLink = document.createElement('a'); + domLink.setAttribute( + 'href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(savedData))); + domLink.setAttribute('download', `${game.game_id}.json`); + domLink.style.display = 'none'; + document.body.appendChild(domLink); + domLink.click(); + document.body.removeChild(domLink); + }) + .catch(exc => page.error(`Error while saving game: ${exc.toString()}`)); + } else { + page.error(`Cannot save this game.`); + } + } + + static builder(page, data) { + return { + title: ContentGame.gameTitle(data), + navigation: [ + ['Help', () => page.loadFancyBox('Help', () => <Help/>)], + ['Load a game from disk', page.loadGameFromDisk], + ['Save game to disk', () => ContentGame.saveGameToDisk(data)], + [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Games`, page.loadGames], + [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Leave game`, () => page.leaveGame(data.game_id)], + [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Logout`, page.logout] + ], + component: <ContentGame page={page} data={data}/> + }; + } + + static getServerWaitFlags(engine) { + const wait = {}; + const controllablePowers = engine.getControllablePowers(); + for (let powerName of controllablePowers) { + wait[powerName] = engine.powers[powerName].wait; + } + return wait; + } + + static getServerOrders(engine) { + const orders = {}; + const controllablePowers = engine.getControllablePowers(); + for (let powerName of controllablePowers) { + const powerOrders = {}; + let countOrders = 0; + const power = engine.powers[powerName]; + for (let orderString of power.orders) { + const serverOrder = new Order(orderString, false); + powerOrders[serverOrder.loc] = serverOrder; + ++countOrders; + } + orders[powerName] = (countOrders || power.order_is_set) ? powerOrders : null; + } + return orders; + } + + static getOrderBuilding(powerName, orderType, orderPath) { + return { + type: orderType, + path: orderPath, + power: powerName, + builder: orderType && ORDER_BUILDER[orderType] + }; + } + + closeFancyBox() { + this.setState({ + fancy_title: null, + fancy_function: null, + on_fancy_close: null, + orderBuildingPath: [] + }); + } + + setSelectedLocation(location, powerName, orderType, orderPath) { + if (!location) + return; + extendOrderBuilding( + powerName, orderType, orderPath, location, + this.onOrderBuilding, this.onOrderBuilt, this.getPage().error + ); + this.setState({ + fancy_title: null, + fancy_function: null, + on_fancy_close: null + }); + } + + setSelectedVia(moveType, powerName, orderPath, location) { + if (!moveType || !['M', 'V'].includes(moveType)) + return; + extendOrderBuilding( + powerName, moveType, orderPath, location, + this.onOrderBuilding, this.onOrderBuilt, this.getPage().error + ); + this.setState({ + fancy_title: null, + fancy_function: null, + on_fancy_close: null + }); + } + + onSelectLocation(possibleLocations, powerName, orderType, orderPath) { + const title = `Select location to continue building order: ${orderPath.join(' ')} ... (press ESC or close button to cancel building)`; + const func = () => (<SelectLocationForm locations={possibleLocations} + onSelect={(location) => this.setSelectedLocation(location, powerName, orderType, orderPath)}/>); + this.setState({ + fancy_title: title, + fancy_function: func, + on_fancy_close: this.closeFancyBox + }); + } + + onSelectVia(location, powerName, orderPath) { + const title = `Select move type for move order: ${orderPath.join(' ')}`; + const func = () => ( + <SelectViaForm onSelect={(moveType) => this.setSelectedVia(moveType, powerName, orderPath, location)}/>); + this.setState({ + fancy_title: title, + fancy_function: func, + on_fancy_close: this.closeFancyBox + }); + } + + __get_orders(engine) { + const orders = ContentGame.getServerOrders(engine); + if (this.state.orders) { + for (let powerName of Object.keys(orders)) { + const serverPowerOrders = orders[powerName]; + const localPowerOrders = this.state.orders[powerName]; + if (localPowerOrders) { + for (let localOrder of Object.values(localPowerOrders)) { + localOrder.local = ( + !serverPowerOrders + || !serverPowerOrders.hasOwnProperty(localOrder.loc) + || serverPowerOrders[localOrder.loc].order !== localOrder.order + ); + } + } + orders[powerName] = localPowerOrders; + } + } + return orders; + } + + __get_wait(engine) { + return this.state.wait ? this.state.wait : ContentGame.getServerWaitFlags(engine); + } + + getMapInfo() { + return this.props.page.availableMaps[this.props.data.map_name]; + } + + clearScheduleTimeout() { + if (this.schedule_timeout_id) { + clearInterval(this.schedule_timeout_id); + this.schedule_timeout_id = null; + } + } + + updateDeadlineTimer() { + const engine = this.props.data; + --engine.deadline_timer; + if (engine.deadline_timer <= 0) { + engine.deadline_timer = 0; + this.clearScheduleTimeout(); + } + this.getPage().setTitle(ContentGame.gameTitle(engine)); + } + + reloadDeadlineTimer(networkGame) { + networkGame.querySchedule() + .then(dataSchedule => { + const schedule = dataSchedule.schedule; + const server_current = schedule.current_time; + const server_end = schedule.time_added + schedule.delay; + const server_remaining = server_end - server_current; + this.props.data.deadline_timer = server_remaining * schedule.time_unit; + if (!this.schedule_timeout_id) + this.schedule_timeout_id = setInterval(() => this.updateDeadlineTimer(), schedule.time_unit * 1000); + }) + .catch(() => { + if (this.props.data.hasOwnProperty('deadline_timer')) + delete this.props.data.deadline_timer; + this.clearScheduleTimeout(); + // this.getPage().error(`Error while updating deadline timer: ${error.toString()}`); + }); + } + + networkGameIsDisplayed(networkGame) { + return this.getPage().pageIsGame(networkGame.local); + } + + notifiedNetworkGame(networkGame, notification) { + if (this.networkGameIsDisplayed(networkGame)) { + const msg = `Game (${networkGame.local.game_id}) received notification ${notification.name}.`; + this.props.page.loadGame(networkGame.local, {info: msg}); + this.reloadDeadlineTimer(networkGame); + } + } + + notifiedPowersControllers(networkGame, notification) { + if (networkGame.local.isPlayerGame() && ( + !networkGame.channel.game_id_to_instances.hasOwnProperty(networkGame.local.game_id) + || !networkGame.channel.game_id_to_instances[networkGame.local.game_id].has(networkGame.local.role) + )) { + // This power game is now invalid. + this.props.page.disconnectGame(networkGame.local.game_id); + if (this.networkGameIsDisplayed(networkGame)) { + this.props.page.loadGames(null, + {error: `Player game ${networkGame.local.game_id}/${networkGame.local.role} was kicked. Deadline over?`}); + } + } else { + this.notifiedNetworkGame(networkGame, notification); + } + } + + notifiedGamePhaseUpdated(networkGame, notification) { + networkGame.getAllPossibleOrders() + .then(allPossibleOrders => { + networkGame.local.setPossibleOrders(allPossibleOrders); + if (this.networkGameIsDisplayed(networkGame)) { + this.getPage().loadGame( + networkGame.local, {info: `Game update (${notification.name}) to ${networkGame.local.phase}.`} + ); + this.__store_orders(null); + this.setState({orders: null, wait: null, messageHighlights: {}}); + this.reloadDeadlineTimer(networkGame); + } + }) + .catch(error => this.getPage().error('Error when updating possible orders: ' + error.toString())); + } + + notifiedLocalStateChange(networkGame) { + networkGame.getAllPossibleOrders() + .then(allPossibleOrders => { + networkGame.local.setPossibleOrders(allPossibleOrders); + if (this.networkGameIsDisplayed(networkGame)) { + this.getPage().loadGame( + networkGame.local, {info: `Possible orders re-loaded.`} + ); + this.reloadDeadlineTimer(networkGame); + } + }) + .catch(error => this.getPage().error('Error when updating possible orders: ' + error.toString())); + } + + notifiedNewGameMessage(networkGame, notification) { + let protagonist = notification.message.sender; + if (notification.message.recipient === 'GLOBAL') + protagonist = notification.message.recipient; + const messageHighlights = Object.assign({}, this.state.messageHighlights); + if (!messageHighlights.hasOwnProperty(protagonist)) + messageHighlights[protagonist] = 1; + else + ++messageHighlights[protagonist]; + this.setState({messageHighlights: messageHighlights}); + this.notifiedNetworkGame(networkGame, notification); + } + + bindCallbacks(networkGame) { + if (!networkGame.callbacksBound) { + networkGame.addOnClearedCenters(this.notifiedLocalStateChange); + networkGame.addOnClearedOrders(this.notifiedLocalStateChange); + networkGame.addOnClearedUnits(this.notifiedLocalStateChange); + networkGame.addOnPowersControllers(this.notifiedPowersControllers); + networkGame.addOnGameMessageReceived(this.notifiedNewGameMessage); + networkGame.addOnGameProcessed(this.notifiedGamePhaseUpdated); + networkGame.addOnGamePhaseUpdate(this.notifiedGamePhaseUpdated); + networkGame.addOnGameStatusUpdate(this.notifiedNetworkGame); + networkGame.addOnOmniscientUpdated(this.notifiedNetworkGame); + networkGame.addOnPowerOrdersUpdate(this.notifiedNetworkGame); + networkGame.addOnPowerOrdersFlag(this.notifiedNetworkGame); + networkGame.addOnPowerVoteUpdated(this.notifiedNetworkGame); + networkGame.addOnPowerWaitFlag(this.notifiedNetworkGame); + networkGame.addOnVoteCountUpdated(this.notifiedNetworkGame); + networkGame.addOnVoteUpdated(this.notifiedNetworkGame); + networkGame.callbacksBound = true; + networkGame.local.markAllMessagesRead(); + } + } + + onChangeCurrentPower(event) { + this.setState({power: event.target.value}); + } + + onChangeMainTab(tab) { + this.setState({tabMain: tab}); + } + + onChangeTabCurrentMessages(tab) { + this.setState({tabCurrentMessages: tab}); + } + + onChangeTabPastMessages(tab) { + this.setState({tabPastMessages: tab}); + } + + sendMessage(networkGame, recipient, body) { + const engine = networkGame.local; + const message = new Message({ + phase: engine.phase, + sender: engine.role, + recipient: recipient, + message: body + }); + const page = this.props.page; + networkGame.sendGameMessage({message: message}) + .then(() => { + page.loadGame(engine, {success: `Message sent: ${JSON.stringify(message)}`}); + }) + .catch(error => page.error(error.toString())); + } + + __store_orders(orders) { + // Save local orders into local storage. + const username = this.props.data.client.channel.username; + const gameID = this.props.data.game_id; + const gamePhase = this.props.data.phase; + if (!orders) { + return DipStorage.clearUserGameOrders(username, gameID); + } + for (let entry of Object.entries(orders)) { + const powerName = entry[0]; + let powerOrdersList = null; + if (entry[1]) { + powerOrdersList = Object.values(entry[1]).map(order => order.order); + } + DipStorage.clearUserGameOrders(username, gameID, powerName); + DipStorage.addUserGameOrders(username, gameID, gamePhase, powerName, powerOrdersList); + } + } + + reloadServerOrders() { + const serverOrders = ContentGame.getServerOrders(this.props.data); + this.__store_orders(serverOrders); + this.setState({orders: serverOrders}); + } + + setOrders() { + const serverOrders = ContentGame.getServerOrders(this.props.data); + const orders = this.__get_orders(this.props.data); + + for (let entry of Object.entries(orders)) { + const powerName = entry[0]; + const localPowerOrders = entry[1] ? Object.values(entry[1]).map(orderEntry => orderEntry.order) : null; + const serverPowerOrders = serverOrders[powerName] ? Object.values(serverOrders[powerName]).map(orderEntry => orderEntry.order) : null; + let same = false; + + if (serverPowerOrders === null) { + // No orders set on server. + if (localPowerOrders === null) + same = true; + // Otherwise, we have local orders set (even empty local orders). + } else if (serverPowerOrders.length === 0) { + // Empty orders set on server. + // If local orders are null or empty, then we assume + // it's the same thing as empty order set on server. + if (localPowerOrders === null || !localPowerOrders.length) + same = true; + // Otherwise, we have local non-empty orders set. + } else { + // Orders set on server. Identical to local orders only if we have exactly same orders on server and locally. + if (localPowerOrders && localPowerOrders.length === serverPowerOrders.length) { + localPowerOrders.sort(); + serverPowerOrders.sort(); + const length = localPowerOrders.length; + same = true; + for (let i = 0; i < length; ++i) { + if (localPowerOrders[i] !== serverPowerOrders[i]) { + same = false; + break; + } + } + } + } + + if (same) { + Diplog.warn(`Orders not changed for ${powerName}.`); + continue; + } + Diplog.info('Sending orders for ' + powerName + ': ' + JSON.stringify(localPowerOrders)); + this.props.data.client.setOrders({power_name: powerName, orders: localPowerOrders || []}) + .then(() => { + this.props.page.success('Orders sent.'); + }) + .catch(err => { + this.props.page.error(err.toString()); + }) + .then(() => { + this.reloadServerOrders(); + }); + } + } + + onProcessGame() { + this.props.data.client.process() + .then(() => this.props.page.success('Game processed.')) + .catch(err => { + this.props.page.error(err.toString()); + }); + } + + onRemoveOrder(powerName, order) { + const orders = this.__get_orders(this.props.data); + if (orders.hasOwnProperty(powerName) + && orders[powerName].hasOwnProperty(order.loc) + && orders[powerName][order.loc].order === order.order) { + delete orders[powerName][order.loc]; + if (!UTILS.javascript.count(orders[powerName])) + orders[powerName] = null; + this.__store_orders(orders); + this.setState({orders: orders}); + } + } + + onRemoveAllOrders() { + const orders = {}; + const controllablePowers = this.props.data.getControllablePowers(); + for (let powerName of controllablePowers) { + orders[powerName] = null; + } + this.__store_orders(orders); + this.setState({orders: orders}); + } + + onOrderBuilding(powerName, path) { + const pathToSave = path.slice(1); + this.props.page.success(`Building order ${pathToSave.join(' ')} ...`); + this.setState({orderBuildingPath: pathToSave}); + } + + onOrderBuilt(powerName, orderString) { + const state = Object.assign({}, this.state); + state.orderBuildingPath = []; + state.fancy_title = null; + state.fancy_function = null; + state.on_fancy_close = null; + if (!orderString) { + Diplog.warn('No order built.'); + this.setState(state); + return; + } + const engine = this.props.data; + const localOrder = new Order(orderString, true); + const allOrders = this.__get_orders(engine); + if (!allOrders.hasOwnProperty(powerName)) { + Diplog.warn(`Unknown power ${powerName}.`); + this.setState(state); + return; + } + + if (!allOrders[powerName]) + allOrders[powerName] = {}; + allOrders[powerName][localOrder.loc] = localOrder; + state.orders = allOrders; + this.props.page.success(`Built order: ${orderString}`); + this.__store_orders(allOrders); + this.setState(state); + } + + onSetNoOrders(powerName) { + const orders = this.__get_orders(this.props.data); + orders[powerName] = {}; + this.__store_orders(orders); + this.setState({orders: orders}); + } + + onChangeOrderType(form) { + this.setState({ + orderBuildingType: form.order_type, + orderBuildingPath: [], + fancy_title: null, + fancy_function: null, + on_fancy_close: null + }); + } + + vote(decision) { + const engine = this.props.data; + const networkGame = engine.client; + const controllablePowers = engine.getControllablePowers(); + const currentPowerName = this.state.power || (controllablePowers.length ? controllablePowers[0] : null); + if (!currentPowerName) + throw new Error(`Internal error: unable to detect current selected power name.`); + networkGame.vote({power_name: currentPowerName, vote: decision}) + .then(() => this.getPage().success(`Vote set to ${decision} for ${currentPowerName}`)) + .catch(error => { + Diplog.error(error.stack); + this.getPage().error(`Error while setting vote for ${currentPowerName}: ${error.toString()}`); + }); + } + + setWaitFlag(waitFlag) { + const engine = this.props.data; + const networkGame = engine.client; + const controllablePowers = engine.getControllablePowers(); + const currentPowerName = this.state.power || (controllablePowers.length ? controllablePowers[0] : null); + if (!currentPowerName) + throw new Error(`Internal error: unable to detect current selected power name.`); + networkGame.setWait(waitFlag, {power_name: currentPowerName}) + .then(() => this.getPage().success(`Wait flag set to ${waitFlag} for ${currentPowerName}`)) + .catch(error => { + Diplog.error(error.stack); + this.getPage().error(`Error while setting wait flag for ${currentPowerName}: ${error.toString()}`); + }); + } + + __change_past_phase(newPhaseIndex, subView) { + this.setState({ + historyPhaseIndex: newPhaseIndex, + historySubView: (subView ? subView : 0), + historyCurrentLoc: null, + historyCurrentOrders: null + }); + } + + onChangePastPhase(event) { + this.__change_past_phase(event.target.value); + } + + onChangePastPhaseIndex(increment) { + const selectObject = document.getElementById('select-past-phase'); + if (selectObject) { + if (!this.state.historyShowOrders) { + // We must change map sub-view before showed phase index. + const currentSubView = this.state.historySubView; + const newSubView = currentSubView + (increment ? 1 : -1); + if (newSubView === 0 || newSubView === 1) { + // Sub-view correctly updated. We don't yet change showed phase. + return this.setState({historySubView: newSubView}); + } + // Sub-view badly updated (either from 0 to -1, or from 1 to 2). We must change phase. + } + // Let's simply increase or decrease index of showed past phase. + const index = selectObject.selectedIndex; + const newIndex = index + (increment ? 1 : -1); + if (newIndex >= 0 && newIndex < selectObject.length) { + selectObject.selectedIndex = newIndex; + this.__change_past_phase(parseInt(selectObject.options[newIndex].value, 10), (increment ? 0 : 1)); + } + } + } + + onIncrementPastPhase(event) { + this.onChangePastPhaseIndex(true); + if (event && event.preventDefault) + event.preventDefault(); + } + + onDecrementPastPhase(event) { + this.onChangePastPhaseIndex(false); + if (event && event.preventDefault) + event.preventDefault(); + } + + displayFirstPastPhase() { + this.__change_past_phase(0, 0); + } + + displayLastPastPhase() { + this.__change_past_phase(-1, 1); + } + + onChangeShowPastOrders(event) { + this.setState({historyShowOrders: event.target.checked, historySubView: 0}); + } + + renderOrders(engine, currentPowerName) { + const serverOrders = ContentGame.getServerOrders(this.props.data); + const orders = this.__get_orders(engine); + const wait = this.__get_wait(engine); + + const render = []; + render.push(<PowerOrder key={currentPowerName} name={currentPowerName} wait={wait[currentPowerName]} + orders={orders[currentPowerName]} + serverCount={serverOrders[currentPowerName] ? UTILS.javascript.count(serverOrders[currentPowerName]) : -1} + onRemove={this.onRemoveOrder}/>); + return render; + } + + onClickMessage(message) { + if (!message.read) { + message.read = true; + let protagonist = message.sender; + if (message.recipient === 'GLOBAL') + protagonist = message.recipient; + this.getPage().loadGame(this.props.data); + if (this.state.messageHighlights.hasOwnProperty(protagonist) && this.state.messageHighlights[protagonist] > 0) { + const messageHighlights = Object.assign({}, this.state.messageHighlights); + --messageHighlights[protagonist]; + this.setState({messageHighlights: messageHighlights}); + } + } + } + + displayLocationOrders(loc, orders) { + this.setState({ + historyCurrentLoc: loc || null, + historyCurrentOrders: orders && orders.length ? orders : null + }); + } + + renderPastMessages(engine) { + const messageChannels = engine.getMessageChannels(); + let tabNames = null; + if (engine.isPlayerGame()) { + tabNames = []; + for (let powerName of Object.keys(engine.powers)) if (powerName !== engine.role) + tabNames.push(powerName); + tabNames.sort(); + tabNames.push('GLOBAL'); + } else { + tabNames = Object.keys(messageChannels); + } + const currentTabId = this.state.tabPastMessages || tabNames[0]; + + return ( + <div className={'panel-messages'} key={'panel-messages'}> + {/* Messages. */} + <Tabs menu={tabNames} titles={tabNames} onChange={this.onChangeTabPastMessages} active={currentTabId}> + {tabNames.map(protagonist => ( + <Tab key={protagonist} className={'game-messages'} display={currentTabId === protagonist}> + {(!messageChannels.hasOwnProperty(protagonist) || !messageChannels[protagonist].length ? + (<div className={'no-game-message'}>No + messages{engine.isPlayerGame() ? ` with ${protagonist}` : ''}.</div>) : + messageChannels[protagonist].map((message, index) => ( + <MessageView key={index} owner={engine.role} message={message} read={true}/> + )) + )} + </Tab> + ))} + </Tabs> + </div> + ); + } + + renderCurrentMessages(engine) { + const messageChannels = engine.getMessageChannels(); + let tabNames = null; + let highlights = null; + if (engine.isPlayerGame()) { + tabNames = []; + for (let powerName of Object.keys(engine.powers)) if (powerName !== engine.role) + tabNames.push(powerName); + tabNames.sort(); + tabNames.push('GLOBAL'); + highlights = this.state.messageHighlights; + } else { + tabNames = Object.keys(messageChannels); + let totalHighlights = 0; + for (let count of Object.values(this.state.messageHighlights)) + totalHighlights += count; + highlights = {messages: totalHighlights}; + } + const unreadMarked = new Set(); + const currentTabId = this.state.tabCurrentMessages || tabNames[0]; + + return ( + <div className={'panel-messages'} key={'panel-messages'}> + {/* Messages. */} + <Tabs menu={tabNames} titles={tabNames} onChange={this.onChangeTabCurrentMessages} active={currentTabId} + highlights={highlights}> + {tabNames.map(protagonist => ( + <Tab id={`panel-current-messages-${protagonist}`} key={protagonist} className={'game-messages'} + display={currentTabId === protagonist}> + {(!messageChannels.hasOwnProperty(protagonist) || !messageChannels[protagonist].length ? + (<div className={'no-game-message'}>No + messages{engine.isPlayerGame() ? ` with ${protagonist}` : ''}.</div>) : + (messageChannels[protagonist].map((message, index) => { + let id = null; + if (!message.read && !unreadMarked.has(protagonist)) { + if (engine.isOmniscientGame() || message.sender !== engine.role) { + unreadMarked.add(protagonist); + id = `${protagonist}-unread`; + } + } + return <MessageView key={index} + owner={engine.role} + message={message} + id={id} + onClick={this.onClickMessage}/>; + })) + )} + </Tab> + ))} + </Tabs> + {/* Link to go to first unread received message. */} + {unreadMarked.has(currentTabId) && ( + <Scrollchor className={'link-unread-message'} + to={`${currentTabId}-unread`} + target={`panel-current-messages-${currentTabId}`}> + Go to 1st unread message + </Scrollchor> + )} + {/* Send form. */} + {engine.isPlayerGame() && ( + <MessageForm sender={engine.role} recipient={currentTabId} onSubmit={form => + this.sendMessage(engine.client, currentTabId, form.message)}/>)} + </div> + ); + } + + renderPastMap(gameEngine, showOrders) { + return <Map key={'past-map'} + id={'past-map'} + game={gameEngine} + mapInfo={this.getMapInfo(gameEngine.map_name)} + onError={this.getPage().error} + onHover={showOrders ? this.displayLocationOrders : null} + showOrders={Boolean(showOrders)} + orders={(gameEngine.order_history.contains(gameEngine.phase) && gameEngine.order_history.get(gameEngine.phase)) || null} + />; + } + + renderCurrentMap(gameEngine, powerName, orderType, orderPath) { + const rawOrders = this.__get_orders(gameEngine); + const orders = {}; + for (let entry of Object.entries(rawOrders)) { + orders[entry[0]] = []; + if (entry[1]) { + for (let orderObject of Object.values(entry[1])) + orders[entry[0]].push(orderObject.order); + } + } + return <Map key={'current-map'} + id={'current-map'} + game={gameEngine} + mapInfo={this.getMapInfo(gameEngine.map_name)} + onError={this.getPage().error} + orderBuilding={ContentGame.getOrderBuilding(powerName, orderType, orderPath)} + onOrderBuilding={this.onOrderBuilding} + onOrderBuilt={this.onOrderBuilt} + showOrders={true} + orders={orders} + onSelectLocation={this.onSelectLocation} + onSelectVia={this.onSelectVia}/>; + } + + renderTabPhaseHistory(toDisplay, initialEngine) { + const pastPhases = initialEngine.state_history.values().map(state => state.name); + if (initialEngine.phase === 'COMPLETED') { + pastPhases.push('COMPLETED'); + } + let phaseIndex = 0; + if (initialEngine.displayed) { + if (this.state.historyPhaseIndex === null || this.state.historyPhaseIndex >= pastPhases.length) { + phaseIndex = pastPhases.length - 1; + } else { + if (this.state.historyPhaseIndex < 0) { + phaseIndex = pastPhases.length + this.state.historyPhaseIndex; + } else { + phaseIndex = this.state.historyPhaseIndex; + } + } + } + const engine = ( + phaseIndex === initialEngine.state_history.size() ? + initialEngine : initialEngine.cloneAt(initialEngine.state_history.keyFromIndex(phaseIndex)) + ); + let orders = {}; + let orderResult = null; + if (engine.order_history.contains(engine.phase)) + orders = engine.order_history.get(engine.phase); + if (engine.result_history.contains(engine.phase)) + orderResult = engine.result_history.get(engine.phase); + let countOrders = 0; + for (let powerOrders of Object.values(orders)) { + if (powerOrders) + countOrders += powerOrders.length; + } + const powerNames = Object.keys(orders); + powerNames.sort(); + + const getOrderResult = (order) => { + if (orderResult) { + const pieces = order.split(/ +/); + const unit = `${pieces[0]} ${pieces[1]}`; + if (orderResult.hasOwnProperty(unit)) { + const resultsToParse = orderResult[unit]; + if (!resultsToParse.length) + resultsToParse.push(''); + const results = []; + for (let r of resultsToParse) { + if (results.length) + results.push(', '); + results.push(<span key={results.length} className={r || 'success'}>{r || 'OK'}</span>); + } + return <span className={'order-result'}> ({results})</span>; + } + } + return ''; + }; + + const orderView = [ + (<form key={1} className={'form-inline mb-4'}> + <Button title={UTILS.html.UNICODE_LEFT_ARROW} onClick={this.onDecrementPastPhase} pickEvent={true} + disabled={phaseIndex === 0}/> + <div className={'form-group'}> + <select className={'form-control custom-select'} + id={'select-past-phase'} + value={phaseIndex} + onChange={this.onChangePastPhase}> + {pastPhases.map((phaseName, index) => <option key={index} value={index}>{phaseName}</option>)} + </select> + </div> + <Button title={UTILS.html.UNICODE_RIGHT_ARROW} onClick={this.onIncrementPastPhase} pickEvent={true} + disabled={phaseIndex === pastPhases.length - 1}/> + <div className={'form-group'}> + <input className={'form-check-input'} id={'show-orders'} type={'checkbox'} + checked={this.state.historyShowOrders} onChange={this.onChangeShowPastOrders}/> + <label className={'form-check-label'} htmlFor={'show-orders'}>Show orders</label> + </div> + </form>), + ((this.state.historyShowOrders && ( + (countOrders && ( + <div key={2} className={'past-orders container'}> + {powerNames.map(powerName => !orders[powerName] || !orders[powerName].length ? '' : ( + <div key={powerName} className={'row'}> + <div className={'past-power-name col-sm-2'}>{powerName}</div> + <div className={'past-power-orders col-sm-10'}> + {orders[powerName].map((order, index) => ( + <div key={index}>{order}{getOrderResult(order)}</div> + ))} + </div> + </div> + ))} + </div> + )) || <div key={2} className={'no-orders'}>No orders for this phase!</div> + )) || '') + ]; + const messageView = this.renderPastMessages(engine); + + let detailsView = null; + if (this.state.historyShowOrders && countOrders) { + detailsView = ( + <Row> + <div className={'col-sm-6'}>{orderView}</div> + <div className={'col-sm-6'}>{messageView}</div> + </Row> + ); + } else { + detailsView = orderView.slice(); + detailsView.push(messageView); + } + + return ( + <Tab id={'tab-phase-history'} display={toDisplay}> + <Row> + <div className={'col-xl'}> + {this.state.historyCurrentOrders && ( + <div className={'history-current-orders'}>{this.state.historyCurrentOrders.join(', ')}</div> + )} + {this.renderPastMap(engine, this.state.historyShowOrders || this.state.historySubView)} + </div> + <div className={'col-xl'}>{detailsView}</div> + </Row> + {toDisplay && <HotKey keys={['arrowleft']} onKeysCoincide={this.onDecrementPastPhase}/>} + {toDisplay && <HotKey keys={['arrowright']} onKeysCoincide={this.onIncrementPastPhase}/>} + {toDisplay && <HotKey keys={['home']} onKeysCoincide={this.displayFirstPastPhase}/>} + {toDisplay && <HotKey keys={['end']} onKeysCoincide={this.displayLastPastPhase}/>} + </Tab> + ); + } + + renderTabCurrentPhase(toDisplay, engine, powerName, orderType, orderPath) { + const powerNames = Object.keys(engine.powers); + powerNames.sort(); + const orderedPowers = powerNames.map(pn => engine.powers[pn]); + return ( + <Tab id={'tab-current-phase'} display={toDisplay}> + <Row> + <div className={'col-xl'}> + {this.renderCurrentMap(engine, powerName, orderType, orderPath)} + </div> + <div className={'col-xl'}> + {/* Orders. */} + <div className={'panel-orders mb-4'}> + <Bar className={'p-2'}> + <strong className={'mr-4'}>Orders:</strong> + <Button title={'reset'} onClick={this.reloadServerOrders}/> + <Button title={'delete all'} onClick={this.onRemoveAllOrders}/> + <Button color={'primary'} title={'update'} onClick={this.setOrders}/> + {(!this.props.data.isPlayerGame() && this.props.data.observer_level === STRINGS.MASTER_TYPE && + <Button color={'danger'} title={'process game'} + onClick={this.onProcessGame}/>) || ''} + </Bar> + <div className={'orders'}>{this.renderOrders(this.props.data, powerName)}</div> + <div className={'table-responsive'}> + <Table className={'table table-striped table-sm'} + caption={'Powers info'} + columns={TABLE_POWER_VIEW} + data={orderedPowers} + wrapper={PowerView.wrap}/> + </div> + </div> + {/* Messages. */} + {this.renderCurrentMessages(engine)} + </div> + </Row> + </Tab> + ); + } + + render() { + const engine = this.props.data; + const phaseType = engine.getPhaseType(); + const controllablePowers = engine.getControllablePowers(); + if (this.props.data.client) + this.bindCallbacks(this.props.data.client); + + if (engine.phase === 'FORMING') + return <main> + <div className={'forming'}>Game not yet started!</div> + </main>; + + const tabNames = []; + const tabTitles = []; + let hasTabPhaseHistory = false; + let hasTabCurrentPhase = false; + if (engine.state_history.size()) { + hasTabPhaseHistory = true; + tabNames.push('phase_history'); + tabTitles.push('Phase history'); + } + if (controllablePowers.length && phaseType) { + hasTabCurrentPhase = true; + tabNames.push('current_phase'); + tabTitles.push('Current phase'); + } + if (!tabNames.length) { + // This should never happen, but let's display this message. + return <main> + <div className={'no-data'}>No data in this game!</div> + </main>; + } + const mainTab = this.state.tabMain && tabNames.includes(this.state.tabMain) ? this.state.tabMain : tabNames[tabNames.length - 1]; + + const currentPowerName = this.state.power || (controllablePowers.length && controllablePowers[0]); + let currentPower = null; + let orderTypeToLocs = null; + let allowedPowerOrderTypes = null; + let orderBuildingType = null; + let buildCount = null; + if (hasTabCurrentPhase) { + currentPower = engine.getPower(currentPowerName); + orderTypeToLocs = engine.getOrderTypeToLocs(currentPowerName); + allowedPowerOrderTypes = Object.keys(orderTypeToLocs); + // canOrder = allowedPowerOrderTypes.length + if (allowedPowerOrderTypes.length) { + POSSIBLE_ORDERS.sortOrderTypes(allowedPowerOrderTypes, phaseType); + if (this.state.orderBuildingType && allowedPowerOrderTypes.includes(this.state.orderBuildingType)) + orderBuildingType = this.state.orderBuildingType; + else + orderBuildingType = allowedPowerOrderTypes[0]; + } + buildCount = engine.getBuildsCount(currentPowerName); + } + + return ( + <main> + {(hasTabCurrentPhase && ( + <div className={'row align-items-center mb-3'}> + <div className={'col-sm-2'}> + {(controllablePowers.length === 1 && + <div className={'power-name'}>{controllablePowers[0]}</div>) || ( + <select className={'form-control custom-select'} id={'current-power'} + value={currentPowerName} onChange={this.onChangeCurrentPower}> + {controllablePowers.map( + powerName => <option key={powerName} value={powerName}>{powerName}</option>)} + </select> + )} + </div> + <div className={'col-sm-10'}> + <PowerActionsForm orderType={orderBuildingType} + orderTypes={allowedPowerOrderTypes} + onChange={this.onChangeOrderType} + onNoOrders={() => this.onSetNoOrders(currentPowerName)} + onSetWaitFlag={() => this.setWaitFlag(!currentPower.wait)} + onVote={this.vote} + role={engine.role} + power={currentPower}/> + </div> + </div> + )) || ''} + {(hasTabCurrentPhase && ( + <div> + {(allowedPowerOrderTypes.length && ( + <span> + <strong>Orderable locations</strong>: {orderTypeToLocs[orderBuildingType].join(', ')} + </span> + )) + || (<strong> No orderable location.</strong>)} + {phaseType === 'A' && ( + (buildCount === null && ( + <strong> (unknown build count)</strong> + )) + || (buildCount === 0 ? ( + <strong> (nothing to build or disband)</strong> + ) : (buildCount > 0 ? ( + <strong> ({buildCount} unit{buildCount > 1 && 's'} may be built)</strong> + ) : ( + <strong> ({-buildCount} unit{buildCount < -1 && 's'} to disband)</strong> + ))) + )} + </div> + )) || ''} + <Tabs menu={tabNames} titles={tabTitles} onChange={this.onChangeMainTab} active={mainTab}> + {/* Tab Phase history. */} + {(hasTabPhaseHistory && this.renderTabPhaseHistory(mainTab === 'phase_history', engine)) || ''} + {/* Tab Current phase. */} + {(hasTabCurrentPhase && this.renderTabCurrentPhase( + mainTab === 'current_phase', + engine, + currentPowerName, + orderBuildingType, + this.state.orderBuildingPath + )) || ''} + </Tabs> + {this.state.fancy_title && ( + <FancyBox title={this.state.fancy_title} onClose={this.state.on_fancy_close}> + {this.state.fancy_function()} + </FancyBox>)} + </main> + ); + } + + componentDidMount() { + super.componentDidMount(); + if (this.props.data.client) + this.reloadDeadlineTimer(this.props.data.client); + this.props.data.displayed = true; + // Try to prevent scrolling when pressing keys Home and End. + document.onkeydown = (event) => { + if (['home', 'end'].includes(event.key.toLowerCase())) { + // Try to prevent scrolling. + if (event.hasOwnProperty('cancelBubble')) + event.cancelBubble = true; + if (event.stopPropagation) + event.stopPropagation(); + if (event.preventDefault) + event.preventDefault(); + } + }; + } + + componentDidUpdate() { + this.props.data.displayed = true; + } + + componentWillUnmount() { + this.clearScheduleTimeout(); + this.props.data.displayed = false; + document.onkeydown = null; + } + +} diff --git a/diplomacy/web/src/gui/diplomacy/contents/content_games.jsx b/diplomacy/web/src/gui/diplomacy/contents/content_games.jsx new file mode 100644 index 0000000..6a62d71 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/contents/content_games.jsx @@ -0,0 +1,140 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import {Content} from "../../core/content"; +import {Tab, Tabs} from "../../core/tabs"; +import {Table} from "../../core/table"; +import {FindForm} from "../forms/find_form"; +import {CreateForm} from "../forms/create_form"; +import {InlineGameView} from "../utils/inline_game_view"; +import {STRINGS} from "../../../diplomacy/utils/strings"; + +const TABLE_LOCAL_GAMES = { + game_id: ['Game ID', 0], + deadline: ['Deadline', 1], + rights: ['Rights', 2], + rules: ['Rules', 3], + players: ['Players/Expected', 4], + status: ['Status', 5], + phase: ['Phase', 6], + join: ['Join', 7], + my_games: ['My Games', 8], +}; + +export class ContentGames extends Content { + + constructor(props) { + super(props); + this.state = {tab: null}; + this.changeTab = this.changeTab.bind(this); + this.onFind = this.onFind.bind(this); + this.onCreate = this.onCreate.bind(this); + this.wrapGameData = this.wrapGameData.bind(this); + } + + static builder(page, data) { + return { + title: 'Games', + navigation: [ + ['load a game from disk', page.loadGameFromDisk], + ['logout', page.logout] + ], + component: <ContentGames page={page} data={data}/> + }; + } + + onFind(form) { + for (let field of ['game_id', 'status', 'include_protected', 'for_omniscience']) + if (!form[field]) + form[field] = null; + this.getPage().channel.listGames(form) + .then((data) => { + this.getPage().success('Found ' + data.length + ' data.'); + this.getPage().addGamesFound(data); + }) + .catch((error) => { + this.getPage().error('Error when looking for distant games: ' + error); + }); + } + + onCreate(form) { + for (let key of Object.keys(form)) { + if (form[key] === '') + form[key] = null; + } + if (form.n_controls !== null) + form.n_controls = parseInt(form.n_controls, 10); + if (form.deadline !== null) + form.deadline = parseInt(form.deadline, 10); + form.rules = ['POWER_CHOICE']; + for (let rule of STRINGS.PUBLIC_RULES) { + const rule_id = `rule_${rule.toLowerCase()}`; + if (form.hasOwnProperty(rule_id)) { + if (form[rule_id]) + form.rules.push(rule); + delete form[rule_id]; + } + } + let networkGame = null; + this.getPage().channel.createGame(form) + .then((game) => { + this.getPage().addToMyGames(game.local); + networkGame = game; + return networkGame.getAllPossibleOrders(); + }) + .then(allPossibleOrders => { + networkGame.local.setPossibleOrders(allPossibleOrders); + this.getPage().loadGame(networkGame.local, {success: 'Game created.'}); + }) + .catch((error) => { + this.getPage().error('Error when creating a game: ' + error); + }); + } + + changeTab(tabIndex) { + this.setState({tab: tabIndex}); + } + + wrapGameData(gameData) { + return new InlineGameView(this.getPage(), gameData); + } + + render() { + const myGames = this.getPage().getMyGames(); + const tab = this.state.tab ? this.state.tab : (myGames.length ? 'my-games' : 'find'); + return ( + <main> + <Tabs menu={['create', 'find', 'my-games']} titles={['Create', 'Find', 'My Games']} + onChange={this.changeTab} active={tab}> + <Tab id="tab-games-create" display={tab === 'create'}> + <CreateForm onSubmit={this.onCreate}/> + </Tab> + <Tab id="tab-games-find" display={tab === 'find'}> + <FindForm onSubmit={this.onFind}/> + <Table className={"table table-striped"} caption={"Games"} columns={TABLE_LOCAL_GAMES} + data={this.getPage().getGamesFound()} wrapper={this.wrapGameData}/> + </Tab> + <Tab id={'tab-my-games'} display={tab === 'my-games'}> + <Table className={"table table-striped"} caption={"My games"} columns={TABLE_LOCAL_GAMES} + data={myGames} wrapper={this.wrapGameData}/> + </Tab> + </Tabs> + </main> + ); + } + +} diff --git a/diplomacy/web/src/gui/diplomacy/forms/connection_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/connection_form.jsx new file mode 100644 index 0000000..49ba381 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/forms/connection_form.jsx @@ -0,0 +1,123 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import {Forms} from "../../core/forms"; +import {UTILS} from "../../../diplomacy/utils/utils"; +import PropTypes from "prop-types"; +import {DipStorage} from "../utils/dipStorage"; + +export class ConnectionForm extends React.Component { + constructor(props) { + super(props); + // Load fields values from local storage. + const initialState = this.initState(); + const savedState = DipStorage.getConnectionForm(); + if (savedState) { + if (savedState.hostname) + initialState.hostname = savedState.hostname; + if (savedState.port) + initialState.port = savedState.port; + if (savedState.username) + initialState.username = savedState.username; + if (savedState.showServerFields) + initialState.showServerFields = savedState.showServerFields; + } + this.state = initialState; + this.updateServerFieldsView = this.updateServerFieldsView.bind(this); + this.onChange = this.onChange.bind(this); + } + + initState() { + return { + hostname: window.location.hostname, + port: (window.location.protocol.toLowerCase() === 'https:') ? 8433 : 8432, + username: '', + password: '', + showServerFields: false + }; + } + + updateServerFieldsView() { + DipStorage.setConnectionshowServerFields(!this.state.showServerFields); + this.setState({showServerFields: !this.state.showServerFields}); + } + + onChange(newState) { + const initialState = this.initState(); + if (newState.hostname !== initialState.hostname) + DipStorage.setConnectionHostname(newState.hostname); + else + DipStorage.setConnectionHostname(null); + if (newState.port !== initialState.port) + DipStorage.setConnectionPort(newState.port); + else + DipStorage.setConnectionPort(null); + if (newState.username !== initialState.username) + DipStorage.setConnectionUsername(newState.username); + else + DipStorage.setConnectionUsername(null); + if (this.props.onChange) + this.props.onChange(newState); + } + + render() { + const onChange = Forms.createOnChangeCallback(this, this.onChange); + const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit); + return ( + <form> + {Forms.createRow( + Forms.createColLabel('username', 'username:'), + <input className={'form-control'} type={'text'} id={'username'} + value={Forms.getValue(this.state, 'username')} onChange={onChange}/> + )} + {Forms.createRow( + Forms.createColLabel('password', 'password:'), + <input className={'form-control'} type={'password'} id={'password'} + value={Forms.getValue(this.state, 'password')} onChange={onChange}/> + )} + <div> + <div className={this.state.showServerFields ? 'mb-2' : 'mb-4'}> + <span className={'button-server'} onClick={this.updateServerFieldsView}> + server settings {this.state.showServerFields ? UTILS.html.UNICODE_BOTTOM_ARROW : UTILS.html.UNICODE_TOP_ARROW} + </span> + </div> + {this.state.showServerFields && ( + <div className={'mb-4'}> + {Forms.createRow( + <label className={'col'} htmlFor={'hostname'}>hostname:</label>, + <input className={'form-control'} type={'text'} id={'hostname'} + value={Forms.getValue(this.state, 'hostname')} onChange={onChange}/> + )} + {Forms.createRow( + <label className={'col'} htmlFor={'port'}>port:</label>, + <input className={'form-control'} type={'number'} id={'port'} + value={Forms.getValue(this.state, 'port')} + onChange={onChange}/> + )} + </div> + )} + </div> + {Forms.createRow('', Forms.createSubmit('connect', true, onSubmit))} + </form> + ); + } +} + +ConnectionForm.propTypes = { + onChange: PropTypes.func, + onSubmit: PropTypes.func +}; diff --git a/diplomacy/web/src/gui/diplomacy/forms/create_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/create_form.jsx new file mode 100644 index 0000000..48c733e --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/forms/create_form.jsx @@ -0,0 +1,95 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import {Forms} from "../../core/forms"; +import {STRINGS} from "../../../diplomacy/utils/strings"; +import PropTypes from "prop-types"; + +export class CreateForm extends React.Component { + constructor(props) { + super(props); + this.state = this.initState(); + } + + initState() { + const state = { + game_id: '', + power_name: '', + n_controls: 7, + deadline: 300, + registration_password: '' + }; + for (let rule of STRINGS.PUBLIC_RULES) + state[`rule_${rule.toLowerCase()}`] = false; + return state; + } + + render() { + const onChange = Forms.createOnChangeCallback(this, this.props.onChange); + const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit); + return ( + <form> + {Forms.createRow( + Forms.createColLabel('game_id', 'Game ID (optional)'), + <input id={'game_id'} className={'form-control'} type={'text'} + value={Forms.getValue(this.state, 'game_id')} onChange={onChange}/> + )} + {Forms.createRow( + Forms.createColLabel('power_name', 'power:'), + <select id={'power_name'} className={'form-control custom-select'} + value={Forms.getValue(this.state, 'power_name')} onChange={onChange}> + {Forms.createSelectOptions(STRINGS.ALL_POWER_NAMES, true)} + </select> + )} + {Forms.createRow( + Forms.createColLabel('n_controls', 'number of required players:'), + <input id={'n_controls'} className={'form-control'} type={'number'} + value={Forms.getValue(this.state, 'n_controls')} onChange={onChange}/> + )} + {Forms.createRow( + Forms.createColLabel('deadline', 'deadline (in seconds)'), + <input id={'deadline'} className={'form-control'} type={'number'} + value={Forms.getValue(this.state, 'deadline')} + onChange={onChange}/> + )} + {Forms.createRow( + Forms.createColLabel('registration_password', 'registration password'), + <input id={'registration_password'} className={'form-control'} type={'password'} + value={Forms.getValue(this.state, 'registration_password')} onChange={onChange}/> + )} + <div><strong>RULES:</strong></div> + <div className={'mb-4'}> + {STRINGS.PUBLIC_RULES.map((rule, index) => ( + <div key={index} className={'form-check-inline'}> + {Forms.createCheckbox( + `rule_${rule.toLowerCase()}`, + rule, + Forms.getValue(this.state, `rule_${rule.toLowerCase()}`), + onChange)} + </div> + ))} + </div> + {Forms.createRow('', Forms.createSubmit('create a game', true, onSubmit))} + </form> + ); + } +} + +CreateForm.propTypes = { + onChange: PropTypes.func, + onSubmit: PropTypes.func +}; diff --git a/diplomacy/web/src/gui/diplomacy/forms/find_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/find_form.jsx new file mode 100644 index 0000000..c73d2b1 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/forms/find_form.jsx @@ -0,0 +1,70 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import {Forms} from "../../core/forms"; +import {STRINGS} from "../../../diplomacy/utils/strings"; +import PropTypes from "prop-types"; + +export class FindForm extends React.Component { + constructor(props) { + super(props); + this.state = this.initState(); + } + + initState() { + return { + game_id: '', + status: '', + include_protected: false, + for_omniscience: false + }; + } + + render() { + const onChange = Forms.createOnChangeCallback(this, this.props.onChange); + const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit); + return ( + <form> + {Forms.createRow( + Forms.createColLabel('game_id', 'game id (should contain):'), + <input className={'form-control'} id={'game_id'} type={'text'} + value={Forms.getValue(this.state, 'game_id')} + onChange={onChange}/> + )} + {Forms.createRow( + Forms.createColLabel('status', 'status:'), + (<select className={'form-control custom-select'} + id={'status'} value={Forms.getValue(this.state, 'status')} onChange={onChange}> + {Forms.createSelectOptions(STRINGS.ALL_GAME_STATUSES, true)} + </select>) + )} + <div className={'form-check'}> + {Forms.createCheckbox('include_protected', 'include protected games.', Forms.getValue(this.state, 'include_protected'), onChange)} + </div> + <div className={'form-check mb-4'}> + {Forms.createCheckbox('for_omniscience', 'for omniscience.', Forms.getValue(this.state, 'for_omniscience'), onChange)} + </div> + {Forms.createRow('', Forms.createSubmit('find games', true, onSubmit))} + </form> + ); + } +} + +FindForm.propTypes = { + onChange: PropTypes.func, + onSubmit: PropTypes.func +}; diff --git a/diplomacy/web/src/gui/diplomacy/forms/join_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/join_form.jsx new file mode 100644 index 0000000..0447280 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/forms/join_form.jsx @@ -0,0 +1,77 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import {Forms} from "../../core/forms"; +import {STRINGS} from "../../../diplomacy/utils/strings"; +import PropTypes from "prop-types"; + +export class JoinForm extends React.Component { + constructor(props) { + super(props); + this.state = this.initState(); + } + + initState() { + return { + [this.getPowerNameID()]: this.getDefaultPowerName(), + [this.getPasswordID()]: '' + }; + } + + getPowerNameID() { + return `power_name_${this.props.game_id}`; + } + + getPasswordID() { + return `registration_password_${this.props.game_id}`; + } + + getDefaultPowerName() { + return (this.props.powers && this.props.powers.length && this.props.powers[0]) || ''; + } + + render() { + const onChange = Forms.createOnChangeCallback(this, this.props.onChange); + const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit); + return ( + <form className={'form-inline'}> + <div className={'form-group'}> + {Forms.createLabel(this.getPowerNameID(), 'Power:')} + <select id={this.getPowerNameID()} className={'from-control custom-select ml-2'} + value={Forms.getValue(this.state, this.getPowerNameID())} onChange={onChange}> + {Forms.createSelectOptions(STRINGS.ALL_POWER_NAMES, true)} + </select> + </div> + <div className={'form-group mx-2'}> + {Forms.createLabel(this.getPasswordID(), '', 'sr-only')} + <input id={this.getPasswordID()} type={'password'} className={'form-control'} + placeholder={'registration password'} + value={Forms.getValue(this.state, this.getPasswordID())} + onChange={onChange}/> + </div> + {Forms.createSubmit('join', false, onSubmit)} + </form> + ); + } +} + +JoinForm.propTypes = { + game_id: PropTypes.string.isRequired, + powers: PropTypes.arrayOf(PropTypes.string), + onChange: PropTypes.func, + onSubmit: PropTypes.func +}; diff --git a/diplomacy/web/src/gui/diplomacy/forms/message_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/message_form.jsx new file mode 100644 index 0000000..a7c377a --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/forms/message_form.jsx @@ -0,0 +1,53 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import {Forms} from "../../core/forms"; +import {UTILS} from "../../../diplomacy/utils/utils"; +import PropTypes from "prop-types"; + +export class MessageForm extends React.Component { + constructor(props) { + super(props); + this.state = this.initState(); + } + + initState() { + return {message: ''}; + } + + render() { + const onChange = Forms.createOnChangeCallback(this, this.props.onChange); + const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit, this.initState()); + return ( + <form> + <div className={'form-group'}> + {Forms.createLabel('message', '', 'sr-only')} + <textarea id={'message'} className={'form-control'} + value={Forms.getValue(this.state, 'message')} onChange={onChange}/> + </div> + {Forms.createSubmit(`send (${this.props.sender} ${UTILS.html.UNICODE_SMALL_RIGHT_ARROW} ${this.props.recipient})`, true, onSubmit)} + </form> + ); + } +} + +MessageForm.propTypes = { + sender: PropTypes.string, + recipient: PropTypes.string, + onChange: PropTypes.func, + onSubmit: PropTypes.func +}; diff --git a/diplomacy/web/src/gui/diplomacy/forms/power_actions_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/power_actions_form.jsx new file mode 100644 index 0000000..33bd763 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/forms/power_actions_form.jsx @@ -0,0 +1,120 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import {Forms} from "../../core/forms"; +import {ORDER_BUILDER} from "../utils/order_building"; +import {STRINGS} from "../../../diplomacy/utils/strings"; +import PropTypes from "prop-types"; +import {Power} from "../../../diplomacy/engine/power"; + +const HotKey = require('react-shortcut'); + +export class PowerActionsForm extends React.Component { + constructor(props) { + super(props); + this.state = this.initState(); + } + + initState() { + return {order_type: this.props.orderType}; + } + + render() { + const onChange = Forms.createOnChangeCallback(this, this.props.onChange); + const onReset = Forms.createOnResetCallback(this, this.props.onChange, this.initState()); + const onSetOrderType = (letter) => { + this.setState({order_type: letter}, () => { + if (this.props.onChange) + this.props.onChange(this.state); + }); + }; + let title = ''; + let titleClass = 'mr-4'; + const header = []; + const votes = []; + if (this.props.orderTypes.length) { + title = 'Create order:'; + header.push(<strong key={'title'} className={titleClass}>{title}</strong>); + header.push(...this.props.orderTypes.map((orderLetter, index) => ( + <div key={index} className={'form-check-inline'}> + {Forms.createRadio('order_type', orderLetter, ORDER_BUILDER[orderLetter].name, this.props.orderType, onChange)} + </div> + ))); + header.push(Forms.createReset('reset', false, onReset)); + } else if (this.props.power.order_is_set) { + title = 'Unorderable power (already locked on server).'; + titleClass += ' neutral'; + header.push(<strong key={'title'} className={titleClass}>{title}</strong>); + } else { + title = 'No orders available for this power.'; + header.push(<strong key={'title'} className={titleClass}>{title}</strong>); + } + if (!this.props.power.order_is_set) { + header.push(Forms.createButton('pass', this.props.onNoOrders)); + } + + if (this.props.role !== STRINGS.OMNISCIENT_TYPE) { + votes.push(<strong key={0} className={'ml-4 mr-2'}>Vote for draw:</strong>); + switch (this.props.power.vote) { + case 'yes': + votes.push(Forms.createButton('no', () => this.props.onVote('no'), 'danger')); + votes.push(Forms.createButton('neutral', () => this.props.onVote('neutral'), 'info')); + break; + case 'no': + votes.push(Forms.createButton('yes', () => this.props.onVote('yes'), 'success')); + votes.push(Forms.createButton('neutral', () => this.props.onVote('neutral'), 'info')); + break; + case 'neutral': + votes.push(Forms.createButton('yes', () => this.props.onVote('yes'), 'success')); + votes.push(Forms.createButton('no', () => this.props.onVote('no'), 'danger')); + break; + default: + votes.push(Forms.createButton('yes', () => this.props.onVote('yes'), 'success')); + votes.push(Forms.createButton('no', () => this.props.onVote('no'), 'danger')); + votes.push(Forms.createButton('neutral', () => this.props.onVote('neutral'), 'info')); + break; + } + } + return ( + <form className={'form-inline power-actions-form'}> + {header} + {Forms.createButton( + (this.props.power.wait ? 'no wait' : 'wait'), + this.props.onSetWaitFlag, + (this.props.power.wait ? 'success' : 'danger') + )} + {votes} + <HotKey keys={['escape']} onKeysCoincide={onReset}/> + {this.props.orderTypes.map((letter, index) => ( + <HotKey key={index} keys={[letter.toLowerCase()]} onKeysCoincide={() => onSetOrderType(letter)}/> + ))} + </form> + ); + } +} + +PowerActionsForm.propTypes = { + orderType: PropTypes.oneOf(Object.keys(ORDER_BUILDER)), + orderTypes: PropTypes.arrayOf(PropTypes.oneOf(Object.keys(ORDER_BUILDER))), + power: PropTypes.instanceOf(Power), + role: PropTypes.string, + onChange: PropTypes.func, + onSubmit: PropTypes.func, + onNoOrders: PropTypes.func, // onNoOrders() + onVote: PropTypes.func, // onVote(voteString) + onSetWaitFlag: PropTypes.func, // onSetWaitFlag(), +}; diff --git a/diplomacy/web/src/gui/diplomacy/forms/select_location_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/select_location_form.jsx new file mode 100644 index 0000000..3c55e49 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/forms/select_location_form.jsx @@ -0,0 +1,36 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import PropTypes from "prop-types"; +import {Button} from "../../core/widgets"; + +export class SelectLocationForm extends React.Component { + render() { + return ( + <div> + {this.props.locations.map((location, index) => ( + <Button key={index} title={location} large={true} onClick={() => this.props.onSelect(location)}/> + ))} + </div> + ); + } +} + +SelectLocationForm.propTypes = { + locations: PropTypes.arrayOf(PropTypes.string).isRequired, + onSelect: PropTypes.func.isRequired // onSelect(location) +}; diff --git a/diplomacy/web/src/gui/diplomacy/forms/select_via_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/select_via_form.jsx new file mode 100644 index 0000000..cc62fe2 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/forms/select_via_form.jsx @@ -0,0 +1,35 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import PropTypes from "prop-types"; +import {Button} from "../../core/widgets"; + +export class SelectViaForm extends React.Component { + render() { + return ( + <div> + <Button title={'regular move (M)'} large={true} onClick={() => this.props.onSelect('M')}/> + <Button title={'move via (V)'} large={true} onClick={() => this.props.onSelect('V')}/> + </div> + ); + } +} + +SelectViaForm.propTypes = { + onSelect: PropTypes.func.isRequired +}; + diff --git a/diplomacy/web/src/gui/diplomacy/map/dom_order_builder.js b/diplomacy/web/src/gui/diplomacy/map/dom_order_builder.js new file mode 100644 index 0000000..8b7072e --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/map/dom_order_builder.js @@ -0,0 +1,278 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {UTILS} from "../../../diplomacy/utils/utils"; +import $ from "jquery"; +import {extendOrderBuilding} from "../utils/order_building"; +import {Diplog} from "../../../diplomacy/utils/diplog"; + +function parseLocation(txt) { + if (txt.length > 2 && txt[1] === ' ' && ['A', 'F'].includes(txt[0])) + return txt.substr(2); + return txt; +} + +export class DOMOrderBuilder { + + constructor(svgElement, onOrderBuilding, onOrderBuilt, onSelectLocation, onSelectVia, onError) { + this.svg = svgElement; + this.cbOrderBuilding = onOrderBuilding; + this.cbOrderBuilt = onOrderBuilt; + this.cbSelectLocation = onSelectLocation; + this.cbSelectVia = onSelectVia; + this.cbError = onError; + + this.game = null; + this.mapData = null; + this.orderBuilding = null; + + this.provinceColors = {}; + this.clickedID = null; + this.clickedNeighbors = []; + + this.onProvinceClick = this.onProvinceClick.bind(this); + this.onLabelClick = this.onLabelClick.bind(this); + this.onUnitClick = this.onUnitClick.bind(this); + } + + saveProvinceColors() { + // Get province colors. + const elements = this.svg.getElementsByTagName('path'); + for (let element of elements) { + this.provinceColors[element.id] = element.getAttribute('class'); + } + } + + provinceNameToMapID(name) { + return `_${name.toLowerCase()}___${this.svg.parentNode.id}`; + } + + mapID(id) { + return `${id}___${this.svg.parentNode.id}`; + } + + onOrderBuilding(svgPath, powerName, orderPath) { + this.cbOrderBuilding(powerName, orderPath); + } + + onOrderBuilt(svgPath, powerName, orderString) { + this.cbOrderBuilt(powerName, orderString); + } + + onError(svgPath, error) { + this.cbError(error.toString()); + } + + handleSvgPath(svgPath) { + const orderBuilding = this.orderBuilding; + if (!orderBuilding.builder) + return this.onError(svgPath, 'No orderable locations.'); + + const province = this.mapData.getProvince(svgPath.id); + if (!province) + return; + + const stepLength = orderBuilding.builder.steps.length; + if (orderBuilding.path.length >= stepLength) + throw new Error(`Order building: current steps count (${orderBuilding.path.length}) should be less than` + + ` expected steps count (${stepLength}) (${orderBuilding.path.join(', ')}).`); + + const lengthAfterClick = orderBuilding.path.length + 1; + let validLocations = []; + const testedPath = [orderBuilding.type].concat(orderBuilding.path); + const value = UTILS.javascript.getTreeValue(this.game.ordersTree, testedPath); + if (value !== null) { + const checker = orderBuilding.builder.steps[lengthAfterClick - 1]; + try { + const possibleLocations = checker(province, orderBuilding.power); + for (let possibleLocation of possibleLocations) { + possibleLocation = possibleLocation.toUpperCase(); + if (value.includes(possibleLocation)) + validLocations.push(possibleLocation); + } + } catch (error) { + return this.onError(svgPath, error); + } + } + if (!validLocations.length) + return this.onError(svgPath, 'Disallowed.'); + + if (validLocations.length > 1 && orderBuilding.type === 'S' && orderBuilding.path.length >= 2) { + // We are building a support order and we have a multiple choice for a location. + // Let's check if next location to choose is a coast. To have a coast: + // - all possible locations must start with same 3 characters. + // - we expect at least province name in possible locations (e.g. 'SPA' for 'SPA/NC'). + // If we have a coast, we will remove province name from possible locations. + let isACoast = true; + let validLocationsNoProvinceName = []; + for (let i = 0; i < validLocations.length; ++i) { + let location = validLocations[i]; + if (i > 0) { + // Compare 3 first letters with previous location. + if (validLocations[i - 1].substring(0, 3).toUpperCase() !== validLocations[i].substring(0, 3).toUpperCase()) { + // No same prefix with previous location. We does not have a coast. + isACoast = false; + break; + } + } + if (location.length !== 3) + validLocationsNoProvinceName.push(location); + } + if (validLocations.length === validLocationsNoProvinceName.length) { + // We have not found province name. + isACoast = false; + } + if (isACoast) { + // We want to choose location in a coastal province. Let's remove province name. + validLocations = validLocationsNoProvinceName; + } + } + + if (validLocations.length > 1) { + if (this.cbSelectLocation) { + return this.cbSelectLocation(validLocations, orderBuilding.power, orderBuilding.type, orderBuilding.path); + } else { + Diplog.warn(`Forced to select first valid location.`); + validLocations = [validLocations[0]]; + } + } + let orderBuildingType = orderBuilding.type; + if (lengthAfterClick === stepLength && orderBuildingType === 'M') { + const moveOrderPath = ['M'].concat(orderBuilding.path, validLocations[0]); + const moveTypes = UTILS.javascript.getTreeValue(this.game.ordersTree, moveOrderPath); + if (moveTypes !== null) { + if (moveTypes.length === 2) { + // This move can be done either regularly or VIA a fleet. Let user choose. + return this.cbSelectVia(validLocations[0], orderBuilding.power, orderBuilding.path); + } else { + orderBuildingType = moveTypes[0]; + } + } + } + this.clickedID = svgPath.id; + + this.cleanBuildingView(); + if (lengthAfterClick < stepLength) + this.renderBuildingView(validLocations[0]); + extendOrderBuilding( + orderBuilding.power, orderBuildingType, orderBuilding.path, validLocations[0], + this.cbOrderBuilding, this.cbOrderBuilt, this.cbError + ); + + } + + getPathFromProvince(province) { + let path = this.svg.getElementById(this.provinceNameToMapID(province.name)); + if (!path) { + for (let alias of province.aliases) { + path = this.svg.getElementById(this.provinceNameToMapID(alias)); + if (path) + break; + } + } + return path; + } + + onProvinceClick(event) { + this.handleSvgPath(event.target); + } + + onLabelClick(event) { + const province = this.mapData.getProvince(event.target.textContent); + if (province) { + const path = this.getPathFromProvince(province); + if (path) + this.handleSvgPath(path); + } + } + + onUnitClick(event) { + const province = this.mapData.getProvince(event.target.getAttribute('diplomacyUnit')); + if (province) { + let path = this.getPathFromProvince(province); + if (!path && province.isCoast()) + path = this.svg.getElementById(this.provinceNameToMapID(province.parent.name)); + if (path) { + this.handleSvgPath(path); + } + } + } + + cleanBuildingView() { + if (this.clickedID) { + const path = this.svg.getElementById(this.clickedID); + if (path) + path.setAttribute('class', this.provinceColors[this.clickedID]); + } + for (let neighborName of this.clickedNeighbors) { + const province = this.mapData.getProvince(neighborName); + if (!province) + continue; + const path = this.getPathFromProvince(province); + if (path) + path.setAttribute('class', this.provinceColors[path.id]); + } + this.clickedNeighbors = []; + } + + renderBuildingView(extraLocation) { + if (this.clickedID) { + const path = this.svg.getElementById(this.clickedID); + if (path) + path.setAttribute('class', 'provinceRed'); + } + const selectedPath = [this.orderBuilding.type].concat(this.orderBuilding.path); + if (extraLocation) + selectedPath.push(extraLocation); + const possibleNeighbors = UTILS.javascript.getTreeValue(this.game.ordersTree, selectedPath); + if (!possibleNeighbors) + return; + this.clickedNeighbors = possibleNeighbors.map(neighbor => parseLocation(neighbor)); + if (this.clickedNeighbors.length) { + for (let neighbor of this.clickedNeighbors) { + let neighborProvince = this.mapData.getProvince(neighbor); + if (!neighborProvince) + throw new Error('Unknown neighbor province ' + neighbor); + let path = this.getPathFromProvince(neighborProvince); + if (!path && neighborProvince.isCoast()) + path = this.getPathFromProvince(neighborProvince.parent); + if (!path) + throw new Error(`Unable to find SVG path related to province ${neighborProvince.name}.`); + path.setAttribute('class', neighborProvince.isWater() ? 'provinceBlue' : 'provinceGreen'); + } + } + } + + update(game, mapData, orderBuilding) { + this.game = game; + this.mapData = mapData; + this.orderBuilding = orderBuilding; + this.saveProvinceColors(); + // If there is a building path, then we are building, so we don't clean anything. + this.cleanBuildingView(); + if (this.orderBuilding.path.length) + this.renderBuildingView(); + // I don't yet know why I should place this here. Maybe because unit are re-rendered manually at every reloading ? + $(`#${this.svg.parentNode.id} svg use[diplomacyUnit]`).click(this.onUnitClick); + } + + init(game, mapData, orderBuilding) { + $(`#${this.svg.parentNode.id} svg path`).click(this.onProvinceClick); + $(`#${this.mapID('BriefLabelLayer')} text`).click(this.onLabelClick); + this.update(game, mapData, orderBuilding); + } + +} diff --git a/diplomacy/web/src/gui/diplomacy/map/dom_past_map.js b/diplomacy/web/src/gui/diplomacy/map/dom_past_map.js new file mode 100644 index 0000000..58fd6c8 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/map/dom_past_map.js @@ -0,0 +1,112 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import $ from "jquery"; + +export class DOMPastMap { + + constructor(svgElement, onHover) { + this.svg = svgElement; + this.cbHover = onHover; + this.game = null; + this.orders = null; + this.mapData = null; + this.onProvinceHover = this.onProvinceHover.bind(this); + this.onLabelHover = this.onLabelHover.bind(this); + this.onUnitHover = this.onUnitHover.bind(this); + } + + provinceNameToMapID(name) { + return `_${name.toLowerCase()}___${this.svg.parentNode.id}`; + } + + mapID(id) { + return `${id}___${this.svg.parentNode.id}`; + } + + onHover(name) { + const orders = []; + if (this.orders) { + for (let powerOrders of Object.values(this.orders)) { + for (let order of powerOrders) { + const pieces = order.split(/ +/); + if (pieces[1].slice(0, 3) === name.toUpperCase().slice(0, 3)) + orders.push(order); + } + } + } + return orders; + } + + handleSvgPath(svgPath) { + const province = this.mapData.getProvince(svgPath.id); + if (province) { + this.cbHover(province.name, this.onHover(province.name)); + } + } + + getPathFromProvince(province) { + let path = this.svg.getElementById(this.provinceNameToMapID(province.name)); + if (!path) { + for (let alias of province.aliases) { + path = this.svg.getElementById(this.provinceNameToMapID(alias)); + if (path) + break; + } + } + return path; + } + + onProvinceHover(event) { + this.handleSvgPath(event.target); + } + + onLabelHover(event) { + const province = this.mapData.getProvince(event.target.textContent); + if (province) { + const path = this.getPathFromProvince(province); + if (path) + this.handleSvgPath(path); + } + } + + onUnitHover(event) { + const province = this.mapData.getProvince(event.target.getAttribute('diplomacyUnit')); + if (province) { + let path = this.getPathFromProvince(province); + if (!path && province.isCoast()) + path = this.svg.getElementById(this.provinceNameToMapID(province.parent.name)); + if (path) { + this.handleSvgPath(path); + } + } + } + + update(game, mapData, orders) { + this.game = game; + this.mapData = mapData; + this.orders = orders; + // I don't yet know why I should place this here. Maybe because unit are re-rendered manually at every reloading ? + $(`#${this.svg.parentNode.id} svg use[diplomacyUnit]`).hover(this.onUnitHover); + } + + init(game, mapData, orders) { + $(`#${this.svg.parentNode.id} svg path`).hover(this.onProvinceHover).mouseleave(() => this.cbHover(null, null)); + $(`#${this.mapID('BriefLabelLayer')} text`).hover(this.onLabelHover); + this.update(game, mapData, orders); + } + +} diff --git a/diplomacy/web/src/gui/diplomacy/map/map.jsx b/diplomacy/web/src/gui/diplomacy/map/map.jsx new file mode 100644 index 0000000..2a2949f --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/map/map.jsx @@ -0,0 +1,94 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import SVG from 'react-inlinesvg'; +import mapSVG from '../../../standard.svg'; +import {Renderer} from "./renderer"; +import {MapData} from "../utils/map_data"; +import {DOMOrderBuilder} from "./dom_order_builder"; +import PropTypes from 'prop-types'; +import {DOMPastMap} from "./dom_past_map"; + +export class Map extends React.Component { + // id: ID of div wrapping SVG map. + // mapInfo: dict + // game: game engine + // onError: callback(error) + // showOrders: bool + + // orderBuilding: dict + // onOrderBuilding: callback(powerName, orderBuildingPath) + // onOrderBuilt: callback(powerName, orderString) + + constructor(props) { + super(props); + this.renderer = null; + this.domOrderBuilder = null; + this.domPastMap = null; + this.initSVG = this.initSVG.bind(this); + } + + initSVG() { + const svg = document.getElementById(this.props.id).getElementsByTagName('svg')[0]; + + const game = this.props.game; + const mapData = new MapData(this.props.mapInfo, game); + this.renderer = new Renderer(svg, game, mapData); + this.renderer.render(this.props.showOrders, this.props.orders); + if (this.props.orderBuilding) { + this.domOrderBuilder = new DOMOrderBuilder( + svg, + this.props.onOrderBuilding, this.props.onOrderBuilt, this.props.onSelectLocation, this.props.onSelectVia, + this.props.onError + ); + this.domOrderBuilder.init(game, mapData, this.props.orderBuilding); + } else if (this.props.onHover) { + this.domPastMap = new DOMPastMap(svg, this.props.onHover); + this.domPastMap.init(game, mapData, this.props.orders); + } + } + + render() { + if (this.renderer) { + const game = this.props.game; + const mapData = new MapData(this.props.mapInfo, game); + this.renderer.update(game, mapData, this.props.showOrders, this.props.orders); + if (this.domOrderBuilder) + this.domOrderBuilder.update(game, mapData, this.props.orderBuilding); + else if (this.domPastMap) + this.domPastMap.update(game, mapData, this.props.orders); + } + const divFactory = ((props, children) => <div id={this.props.id} {...props}>{children}</div>); + return <SVG wrapper={divFactory} uniquifyIDs={true} uniqueHash={this.props.id} src={mapSVG} + onLoad={this.initSVG} onError={err => this.props.onError(err.message)}>Game map</SVG>; + } +} + +Map.propTypes = { + id: PropTypes.string, + showOrders: PropTypes.bool, + orders: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)), + onSelectLocation: PropTypes.func, + onSelectVia: PropTypes.func, + game: PropTypes.object, + mapInfo: PropTypes.object, + orderBuilding: PropTypes.object, + onOrderBuilding: PropTypes.func, + onOrderBuilt: PropTypes.func, + onError: PropTypes.func, + onHover: PropTypes.func, +}; diff --git a/diplomacy/web/src/gui/diplomacy/map/renderer.js b/diplomacy/web/src/gui/diplomacy/map/renderer.js new file mode 100644 index 0000000..e2586af --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/map/renderer.js @@ -0,0 +1,615 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import $ from "jquery"; + +const ARMY = 'Army'; +const FLEET = 'Fleet'; +// SVG tag names. +const PREFIX_TAG = 'jdipNS'.toLowerCase(); +const TAG_ORDERDRAWING = 'jdipNS:ORDERDRAWING'.toLowerCase(); +const TAG_POWERCOLORS = 'jdipNS:POWERCOLORS'.toLowerCase(); +const TAG_POWERCOLOR = 'jdipNS:POWERCOLOR'.toLowerCase(); +const TAG_SYMBOLSIZE = 'jdipNS:SYMBOLSIZE'.toLowerCase(); +const TAG_PROVINCE_DATA = 'jdipNS:PROVINCE_DATA'.toLowerCase(); +const TAG_PROVINCE = 'jdipNS:PROVINCE'.toLowerCase(); +const TAG_UNIT = 'jdipNS:UNIT'.toLowerCase(); +const TAG_DISLODGED_UNIT = 'jdipNS:DISLODGED_UNIT'.toLowerCase(); +const TAG_SUPPLY_CENTER = 'jdipNS:SUPPLY_CENTER'.toLowerCase(); +const TAG_DISPLAY = 'jdipNS:DISPLAY'.toLowerCase(); + +function attr(node, name) { + return node.attributes[name].value; +} + +function offset(floatString, offset) { + return "" + (parseFloat(floatString) + offset); +} + +export class Renderer { + constructor(svgDomElement, game, mapData) { + this.svg = svgDomElement; + this.game = game; + this.mapData = mapData; + this.metadata = { + color: {}, + symbol_size: {}, + orders: {}, + coord: {} + }; + this.initialInfluences = {}; + this.__load_metadata(); + this.__save_initial_influences(); + } + + __hashed_id(id) { + return `${id}___${this.svg.parentNode.id}`; + } + + __svg_element_from_id(id) { + const hashedID = this.__hashed_id(id); + const element = this.svg.getElementById(hashedID); + if (!element) + throw new Error(`Unable to find ID ${id} (looked for hashed ID ${hashedID})`); + return element; + } + + __load_metadata() { + // Order drawings. + const order_drawings = this.svg.getElementsByTagName(TAG_ORDERDRAWING); + if (!order_drawings.length) + throw new Error('Unable to find order drawings (tag ' + TAG_ORDERDRAWING + ') in SVG map.'); + for (let order_drawing of order_drawings) { + for (let child_node of order_drawing.childNodes) { + if (child_node.nodeName === TAG_POWERCOLORS) { + // Power colors. + for (let power_color of child_node.childNodes) { + if (power_color.nodeName === TAG_POWERCOLOR) { + this.metadata.color[attr(power_color, 'power').toUpperCase()] = attr(power_color, 'color'); + } + } + } else if (child_node.nodeName === TAG_SYMBOLSIZE) { + // Symbol size. + this.metadata.symbol_size[attr(child_node, 'name')] = [attr(child_node, 'height'), attr(child_node, 'width')]; + } else if (child_node.nodeName.startsWith(PREFIX_TAG)) { + // Order type. + const order_type = child_node.nodeName.replace(PREFIX_TAG + ':', ''); + this.metadata.orders[order_type] = {}; + for (let attribute of child_node.attributes) { + if (!attribute.name.includes(':')) { + this.metadata.orders[order_type][attribute.name] = attribute.value; + } + } + } + } + } + // Object coordinates. + const all_province_data = this.svg.getElementsByTagName(TAG_PROVINCE_DATA); + if (!all_province_data.length) + throw new Error('Unable to find province data in SVG map (tag ' + TAG_PROVINCE_DATA + ').'); + for (let province_data of all_province_data) { + for (let child_node of province_data.childNodes) { + // Province. + if (child_node.nodeName === TAG_PROVINCE) { + const province = attr(child_node, 'name').toUpperCase().replace('-', '/'); + this.metadata.coord[province] = {}; + for (let coord_node of child_node.childNodes) { + if (coord_node.nodeName === TAG_UNIT) { + this.metadata.coord[province].unit = [attr(coord_node, 'x'), attr(coord_node, 'y')]; + } else if (coord_node.nodeName === TAG_DISLODGED_UNIT) { + this.metadata.coord[province].disl = [attr(coord_node, 'x'), attr(coord_node, 'y')]; + } else if (coord_node.nodeName === TAG_SUPPLY_CENTER) { + this.metadata.coord[province].sc = [attr(coord_node, 'x'), attr(coord_node, 'y')]; + } + } + } + } + } + // Deleting. + this.svg.removeChild(this.svg.getElementsByTagName(TAG_DISPLAY)[0]); + this.svg.removeChild(this.svg.getElementsByTagName(TAG_ORDERDRAWING)[0]); + this.svg.removeChild(this.svg.getElementsByTagName(TAG_PROVINCE_DATA)[0]); + + // (this code was previously in render()) + // Removing mouse layer. + this.svg.removeChild(this.__svg_element_from_id('MouseLayer')); + } + + __save_initial_influences() { + const mapLayer = this.__svg_element_from_id('MapLayer'); + if (!mapLayer) + throw new Error('Unable to find map layer.'); + for (let element of mapLayer.childNodes) { + if (element.tagName === 'path') { + this.initialInfluences[element.id] = element.getAttribute('class'); + } + } + } + + __restore_initial_influences() { + for (let id of Object.keys(this.initialInfluences)) { + const className = this.initialInfluences[id]; + this.svg.getElementById(id).setAttribute('class', className); + } + } + + __set_current_phase() { + const current_phase = (this.game.phase[0] === '?' || this.game.phase === 'COMPLETED') ? 'FINAL' : this.game.phase; + const phase_display = this.__svg_element_from_id('CurrentPhase'); + if (phase_display) { + phase_display.childNodes[0].nodeValue = current_phase; + } + } + + __set_note(note1, note2) { + note1 = note1 || ''; + note2 = note2 || ''; + const display_note1 = this.__svg_element_from_id('CurrentNote'); + const display_note2 = this.__svg_element_from_id('CurrentNote2'); + if (display_note1) + display_note1.childNodes[0].nodeValue = note1; + if (display_note2) + display_note2.childNodes[0].nodeValue = note2; + } + + __add_unit(unit, power_name, is_dislogged) { + const split_unit = unit.split(/ +/); + const unit_type = split_unit[0]; + const loc = split_unit[1]; + const dislogged_type = is_dislogged ? 'disl' : 'unit'; + const symbol = unit_type === 'F' ? FLEET : ARMY; + const loc_x = offset(this.metadata.coord[loc][dislogged_type][0], -11.5); + const loc_y = offset(this.metadata.coord[loc][dislogged_type][1], -10.0); + // Helpful link about creating SVG elements: https://stackoverflow.com/a/25949237 + const node = document.createElementNS("http://www.w3.org/2000/svg", 'use'); + node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#' + this.__hashed_id((is_dislogged ? 'Dislodged' : '') + symbol)); + node.setAttribute('x', loc_x); + node.setAttribute('y', loc_y); + node.setAttribute('height', this.metadata.symbol_size[symbol][0]); + node.setAttribute('width', this.metadata.symbol_size[symbol][1]); + node.setAttribute('class', 'unit' + power_name.toLowerCase()); + node.setAttribute('diplomacyUnit', loc); + const parent_node = this.__svg_element_from_id(is_dislogged ? 'DislodgedUnitLayer' : 'UnitLayer'); + if (parent_node) + parent_node.appendChild(node); + } + + __add_supply_center(loc, power_name) { + const symbol = 'SupplyCenter'; + const loc_x = offset(this.metadata.coord[loc]['sc'][0], -8.5); + const loc_y = offset(this.metadata.coord[loc]['sc'][1], -11.0); + const node = document.createElementNS("http://www.w3.org/2000/svg", 'use'); + node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#' + this.__hashed_id(symbol)); + node.setAttribute('x', loc_x); + node.setAttribute('y', loc_y); + node.setAttribute('height', this.metadata.symbol_size[symbol][0]); + node.setAttribute('width', this.metadata.symbol_size[symbol][1]); + node.setAttribute('class', power_name ? ('sc' + power_name.toLowerCase()) : 'scnopower'); + const parent_node = this.__svg_element_from_id('SupplyCenterLayer'); + if (parent_node) + parent_node.appendChild(node); + } + + __set_influence(loc, power_name) { + loc = loc.toUpperCase().substr(0, 3); + if (!['LAND', 'COAST'].includes(this.mapData.getProvince(loc).type)) + return; + const path = this.__svg_element_from_id('_' + loc.toLowerCase()); + if (!path || path.nodeName !== 'path') { + throw new Error(`Unable to find SVG path for loc ${loc}, got ${path ? path.nodeName : '(nothing)'}`); + } + path.setAttribute('class', power_name ? power_name.toLowerCase() : 'nopower'); + } + + issueHoldOrder(loc, power_name) { + const polygon_coord = []; + const loc_x = offset(this.metadata['coord'][loc]['unit'][0], 8.5); + const loc_y = offset(this.metadata['coord'][loc]['unit'][1], 9.5); + for (let ofs of [ + [13.8, -33.3], [33.3, -13.8], [33.3, 13.8], [13.8, 33.3], [-13.8, 33.3], + [-33.3, 13.8], [-33.3, -13.8], [-13.8, -33.3]] + ) { + polygon_coord.push(offset(loc_x, ofs[0]) + ',' + offset(loc_y, ofs[1])); + } + const g_node = document.createElementNS("http://www.w3.org/2000/svg", 'g'); + const poly_1 = document.createElementNS("http://www.w3.org/2000/svg", 'polygon'); + const poly_2 = document.createElementNS("http://www.w3.org/2000/svg", 'polygon'); + poly_1.setAttribute('stroke-width', '10'); + poly_1.setAttribute('class', 'varwidthshadow'); + poly_1.setAttribute('points', polygon_coord.join(' ')); + poly_2.setAttribute('stroke-width', '6'); + poly_2.setAttribute('class', 'varwidthorder'); + poly_2.setAttribute('points', polygon_coord.join(' ')); + poly_2.setAttribute('stroke', this.metadata['color'][power_name]); + g_node.appendChild(poly_1); + g_node.appendChild(poly_2); + const orderLayer = this.__svg_element_from_id('Layer1'); + if (!orderLayer) + throw new Error(`Unable to find svg order layer.`); + orderLayer.appendChild(g_node); + } + + issueMoveOrder(src_loc, dest_loc, power_name) { + let src_loc_x = 0; + let src_loc_y = 0; + const phaseType = this.game.getPhaseType(); + if (phaseType === 'R') { + src_loc_x = offset(this.metadata.coord[src_loc]['unit'][0], -2.5); + src_loc_y = offset(this.metadata.coord[src_loc]['unit'][1], -2.5); + } else { + src_loc_x = offset(this.metadata.coord[src_loc]['unit'][0], 10); + src_loc_y = offset(this.metadata.coord[src_loc]['unit'][1], 10); + } + let dest_loc_x = offset(this.metadata.coord[dest_loc]['unit'][0], 10); + let dest_loc_y = offset(this.metadata.coord[dest_loc]['unit'][1], 10); + + // Adjusting destination + const delta_x = parseFloat(dest_loc_x) - parseFloat(src_loc_x); + const delta_y = parseFloat(dest_loc_y) - parseFloat(src_loc_y); + const vector_length = Math.sqrt(delta_x * delta_x + delta_y * delta_y); + dest_loc_x = '' + Math.round((parseFloat(src_loc_x) + (vector_length - 30.) / vector_length * delta_x) * 100.) / 100.; + dest_loc_y = '' + Math.round((parseFloat(src_loc_y) + (vector_length - 30.) / vector_length * delta_y) * 100.) / 100.; + + // Creating nodes. + const g_node = document.createElementNS("http://www.w3.org/2000/svg", 'g'); + const line_with_shadow = document.createElementNS("http://www.w3.org/2000/svg", 'line'); + const line_with_arrow = document.createElementNS("http://www.w3.org/2000/svg", 'line'); + line_with_shadow.setAttribute('x1', src_loc_x); + line_with_shadow.setAttribute('y1', src_loc_y); + line_with_shadow.setAttribute('x2', dest_loc_x); + line_with_shadow.setAttribute('y2', dest_loc_y); + line_with_shadow.setAttribute('class', 'varwidthshadow'); + line_with_shadow.setAttribute('stroke-width', '10'); + line_with_arrow.setAttribute('x1', src_loc_x); + line_with_arrow.setAttribute('y1', src_loc_y); + line_with_arrow.setAttribute('x2', dest_loc_x); + line_with_arrow.setAttribute('y2', dest_loc_y); + line_with_arrow.setAttribute('class', 'varwidthorder'); + line_with_arrow.setAttribute('marker-end', 'url(#' + this.__hashed_id('arrow') + ')'); + line_with_arrow.setAttribute('stroke', this.metadata.color[power_name]); + line_with_arrow.setAttribute('stroke-width', '6'); + g_node.appendChild(line_with_shadow); + g_node.appendChild(line_with_arrow); + const orderLayer = this.__svg_element_from_id('Layer1'); + if (!orderLayer) + throw new Error(`Unable to find svg order layer.`); + orderLayer.appendChild(g_node); + } + + issueSupportMoveOrder(loc, src_loc, dest_loc, power_name) { + const loc_x = offset(this.metadata['coord'][loc]['unit'][0], 10); + const loc_y = offset(this.metadata['coord'][loc]['unit'][1], 10); + const src_loc_x = offset(this.metadata['coord'][src_loc]['unit'][0], 10); + const src_loc_y = offset(this.metadata['coord'][src_loc]['unit'][1], 10); + let dest_loc_x = offset(this.metadata['coord'][dest_loc]['unit'][0], 10); + let dest_loc_y = offset(this.metadata['coord'][dest_loc]['unit'][1], 10); + + // Adjusting destination + const delta_x = parseFloat(dest_loc_x) - parseFloat(src_loc_x); + const delta_y = parseFloat(dest_loc_y) - parseFloat(src_loc_y); + const vector_length = Math.sqrt(delta_x * delta_x + delta_y * delta_y); + dest_loc_x = '' + Math.round((parseFloat(src_loc_x) + (vector_length - 30.) / vector_length * delta_x) * 100.) / 100.; + dest_loc_y = '' + Math.round((parseFloat(src_loc_y) + (vector_length - 30.) / vector_length * delta_y) * 100.) / 100.; + + const g_node = document.createElementNS("http://www.w3.org/2000/svg", 'g'); + const path_with_shadow = document.createElementNS("http://www.w3.org/2000/svg", 'path'); + const path_with_arrow = document.createElementNS("http://www.w3.org/2000/svg", 'path'); + path_with_shadow.setAttribute('class', 'shadowdash'); + path_with_shadow.setAttribute('d', `M ${loc_x},${loc_y} C ${src_loc_x},${src_loc_y} ${src_loc_x},${src_loc_y} ${dest_loc_x},${dest_loc_y}`); + path_with_arrow.setAttribute('class', 'supportorder'); + path_with_arrow.setAttribute('marker-end', 'url(#' + this.__hashed_id('arrow') + ')'); + path_with_arrow.setAttribute('stroke', this.metadata['color'][power_name]); + path_with_arrow.setAttribute('d', `M ${loc_x},${loc_y} C ${src_loc_x},${src_loc_y} ${src_loc_x},${src_loc_y} ${dest_loc_x},${dest_loc_y}`); + g_node.appendChild(path_with_shadow); + g_node.appendChild(path_with_arrow); + const orderLayer = this.__svg_element_from_id('Layer2'); + if (!orderLayer) + throw new Error(`Unable to find svg order layer.`); + orderLayer.appendChild(g_node); + } + + issueSupportHoldOrder(loc, dest_loc, power_name) { + const loc_x = offset(this.metadata['coord'][loc]['unit'][0], 10); + const loc_y = offset(this.metadata['coord'][loc]['unit'][1], 10); + let dest_loc_x = offset(this.metadata['coord'][dest_loc]['unit'][0], 10); + let dest_loc_y = offset(this.metadata['coord'][dest_loc]['unit'][1], 10); + + const delta_x = parseFloat(dest_loc_x) - parseFloat(loc_x); + const delta_y = parseFloat(dest_loc_y) - parseFloat(loc_y); + const vector_length = Math.sqrt(delta_x * delta_x + delta_y * delta_y); + dest_loc_x = '' + Math.round((parseFloat(loc_x) + (vector_length - 35.) / vector_length * delta_x) * 100.) / 100.; + dest_loc_y = '' + Math.round((parseFloat(loc_y) + (vector_length - 35.) / vector_length * delta_y) * 100.) / 100.; + + const polygon_coord = []; + const poly_loc_x = offset(this.metadata['coord'][dest_loc]['unit'][0], 8.5); + const poly_loc_y = offset(this.metadata['coord'][dest_loc]['unit'][1], 9.5); + for (let ofs of [ + [15.9, -38.3], [38.3, -15.9], [38.3, 15.9], [15.9, 38.3], [-15.9, 38.3], [-38.3, 15.9], + [-38.3, -15.9], [-15.9, -38.3] + ]) { + polygon_coord.push(offset(poly_loc_x, ofs[0]) + ',' + offset(poly_loc_y, ofs[1])); + } + const g_node = document.createElementNS("http://www.w3.org/2000/svg", 'g'); + const shadow_line = document.createElementNS("http://www.w3.org/2000/svg", 'line'); + const support_line = document.createElementNS("http://www.w3.org/2000/svg", 'line'); + const shadow_poly = document.createElementNS("http://www.w3.org/2000/svg", 'polygon'); + const support_poly = document.createElementNS("http://www.w3.org/2000/svg", 'polygon'); + shadow_line.setAttribute('x1', loc_x); + shadow_line.setAttribute('y1', loc_y); + shadow_line.setAttribute('x2', dest_loc_x); + shadow_line.setAttribute('y2', dest_loc_y); + shadow_line.setAttribute('class', 'shadowdash'); + support_line.setAttribute('x1', loc_x); + support_line.setAttribute('y1', loc_y); + support_line.setAttribute('x2', dest_loc_x); + support_line.setAttribute('y2', dest_loc_y); + support_line.setAttribute('class', 'supportorder'); + support_line.setAttribute('stroke', this.metadata['color'][power_name]); + shadow_poly.setAttribute('class', 'shadowdash'); + shadow_poly.setAttribute('points', polygon_coord.join(' ')); + support_poly.setAttribute('class', 'supportorder'); + support_poly.setAttribute('points', polygon_coord.join(' ')); + support_poly.setAttribute('stroke', this.metadata['color'][power_name]); + g_node.appendChild(shadow_line); + g_node.appendChild(support_line); + g_node.appendChild(shadow_poly); + g_node.appendChild(support_poly); + const orderLayer = this.__svg_element_from_id('Layer2'); + if (!orderLayer) + throw new Error(`Unable to find svg order layer.`); + orderLayer.appendChild(g_node); + } + + issueConvoyOrder(loc, src_loc, dest_loc, power_name) { + const loc_x = offset(this.metadata['coord'][loc]['unit'][0], 10); + const loc_y = offset(this.metadata['coord'][loc]['unit'][1], 10); + const src_loc_x = offset(this.metadata['coord'][src_loc]['unit'][0], 10); + const src_loc_y = offset(this.metadata['coord'][src_loc]['unit'][1], 10); + let dest_loc_x = offset(this.metadata['coord'][dest_loc]['unit'][0], 10); + let dest_loc_y = offset(this.metadata['coord'][dest_loc]['unit'][1], 10); + + const src_delta_x = parseFloat(src_loc_x) - parseFloat(loc_x); + const src_delta_y = parseFloat(src_loc_y) - parseFloat(loc_y); + const src_vector_length = Math.sqrt(src_delta_x * src_delta_x + src_delta_y * src_delta_y); + const src_loc_x_1 = '' + Math.round((parseFloat(loc_x) + (src_vector_length - 30.) / src_vector_length * src_delta_x) * 100.) / 100.; + const src_loc_y_1 = '' + Math.round((parseFloat(loc_y) + (src_vector_length - 30.) / src_vector_length * src_delta_y) * 100.) / 100.; + + let dest_delta_x = parseFloat(src_loc_x) - parseFloat(dest_loc_x); + let dest_delta_y = parseFloat(src_loc_y) - parseFloat(dest_loc_y); + let dest_vector_length = Math.sqrt(dest_delta_x * dest_delta_x + dest_delta_y * dest_delta_y); + const src_loc_x_2 = '' + Math.round((parseFloat(dest_loc_x) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_x) * 100.) / 100.; + const src_loc_y_2 = '' + Math.round((parseFloat(dest_loc_y) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_y) * 100.) / 100.; + + dest_delta_x = parseFloat(dest_loc_x) - parseFloat(src_loc_x); + dest_delta_y = parseFloat(dest_loc_y) - parseFloat(src_loc_y); + dest_vector_length = Math.sqrt(dest_delta_x * dest_delta_x + dest_delta_y * dest_delta_y); + dest_loc_x = '' + Math.round((parseFloat(src_loc_x) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_x) * 100.) / 100.; + dest_loc_y = '' + Math.round((parseFloat(src_loc_y) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_y) * 100.) / 100.; + + const triangle_coord = []; + const triangle_loc_x = offset(this.metadata['coord'][src_loc]['unit'][0], 10); + const triangle_loc_y = offset(this.metadata['coord'][src_loc]['unit'][1], 10); + for (let ofs of [[0, -38.3], [33.2, 19.1], [-33.2, 19.1]]) { + triangle_coord.push(offset(triangle_loc_x, ofs[0]) + ',' + offset(triangle_loc_y, ofs[1])); + } + + const g_node = document.createElementNS("http://www.w3.org/2000/svg", 'g'); + const src_shadow_line = document.createElementNS("http://www.w3.org/2000/svg", 'line'); + const dest_shadow_line = document.createElementNS("http://www.w3.org/2000/svg", 'line'); + const src_convoy_line = document.createElementNS("http://www.w3.org/2000/svg", 'line'); + const dest_convoy_line = document.createElementNS("http://www.w3.org/2000/svg", 'line'); + const shadow_poly = document.createElementNS("http://www.w3.org/2000/svg", 'polygon'); + const convoy_poly = document.createElementNS("http://www.w3.org/2000/svg", 'polygon'); + src_shadow_line.setAttribute('x1', loc_x); + src_shadow_line.setAttribute('y1', loc_y); + src_shadow_line.setAttribute('x2', src_loc_x_1); + src_shadow_line.setAttribute('y2', src_loc_y_1); + src_shadow_line.setAttribute('class', 'shadowdash'); + + dest_shadow_line.setAttribute('x1', src_loc_x_2); + dest_shadow_line.setAttribute('y1', src_loc_y_2); + dest_shadow_line.setAttribute('x2', dest_loc_x); + dest_shadow_line.setAttribute('y2', dest_loc_y); + dest_shadow_line.setAttribute('class', 'shadowdash'); + + src_convoy_line.setAttribute('x1', loc_x); + src_convoy_line.setAttribute('y1', loc_y); + src_convoy_line.setAttribute('x2', src_loc_x_1); + src_convoy_line.setAttribute('y2', src_loc_y_1); + src_convoy_line.setAttribute('class', 'convoyorder'); + src_convoy_line.setAttribute('stroke', this.metadata['color'][power_name]); + + dest_convoy_line.setAttribute('x1', src_loc_x_2); + dest_convoy_line.setAttribute('y1', src_loc_y_2); + dest_convoy_line.setAttribute('x2', dest_loc_x); + dest_convoy_line.setAttribute('y2', dest_loc_y); + dest_convoy_line.setAttribute('class', 'convoyorder'); + dest_convoy_line.setAttribute('marker-end', 'url(#' + this.__hashed_id('arrow') + ')'); + + dest_convoy_line.setAttribute('stroke', this.metadata['color'][power_name]); + + shadow_poly.setAttribute('class', 'shadowdash'); + shadow_poly.setAttribute('points', triangle_coord.join(' ')); + + convoy_poly.setAttribute('class', 'convoyorder'); + convoy_poly.setAttribute('points', triangle_coord.join(' ')); + convoy_poly.setAttribute('stroke', this.metadata['color'][power_name]); + + g_node.appendChild(src_shadow_line); + g_node.appendChild(dest_shadow_line); + g_node.appendChild(src_convoy_line); + g_node.appendChild(dest_convoy_line); + g_node.appendChild(shadow_poly); + g_node.appendChild(convoy_poly); + + const orderLayer = this.__svg_element_from_id('Layer2'); + if (!orderLayer) + throw new Error(`Unable to find svg order layer.`); + orderLayer.appendChild(g_node); + } + + issueBuildOrder(unit_type, loc, power_name) { + const loc_x = offset(this.metadata['coord'][loc]['unit'][0], -11.5); + const loc_y = offset(this.metadata['coord'][loc]['unit'][1], -10.); + const build_loc_x = offset(this.metadata['coord'][loc]['unit'][0], -20.5); + const build_loc_y = offset(this.metadata['coord'][loc]['unit'][1], -20.5); + const symbol = unit_type === 'A' ? ARMY : FLEET; + const build_symbol = 'BuildUnit'; + const g_node = document.createElementNS("http://www.w3.org/2000/svg", 'g'); + const symbol_node = document.createElementNS("http://www.w3.org/2000/svg", 'use'); + const build_node = document.createElementNS("http://www.w3.org/2000/svg", 'use'); + symbol_node.setAttribute('x', loc_x); + symbol_node.setAttribute('y', loc_y); + symbol_node.setAttribute('height', this.metadata['symbol_size'][symbol][0]); + symbol_node.setAttribute('width', this.metadata['symbol_size'][symbol][1]); + symbol_node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#' + this.__hashed_id(symbol)); + symbol_node.setAttribute('class', `unit${power_name.toLowerCase()}`); + build_node.setAttribute('x', build_loc_x); + build_node.setAttribute('y', build_loc_y); + build_node.setAttribute('height', this.metadata['symbol_size'][build_symbol][0]); + build_node.setAttribute('width', this.metadata['symbol_size'][build_symbol][1]); + build_node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#' + this.__hashed_id(build_symbol)); + g_node.appendChild(build_node); + g_node.appendChild(symbol_node); + const orderLayer = this.__svg_element_from_id('HighestOrderLayer'); + if (!orderLayer) + throw new Error(`Unable to find svg order layer.`); + orderLayer.appendChild(g_node); + } + + issueDisbandOrder(loc) { + const phaseType = this.game.getPhaseType(); + let loc_x = 0; + let loc_y = 0; + if (phaseType === 'R') { + loc_x = offset(this.metadata['coord'][loc]['unit'][0], -29.); + loc_y = offset(this.metadata['coord'][loc]['unit'][1], -27.5); + } else { + loc_x = offset(this.metadata['coord'][loc]['unit'][0], -16.5); + loc_y = offset(this.metadata['coord'][loc]['unit'][1], -15.); + } + const symbol = 'RemoveUnit'; + const g_node = document.createElementNS("http://www.w3.org/2000/svg", 'g'); + const symbol_node = document.createElementNS("http://www.w3.org/2000/svg", 'use'); + symbol_node.setAttribute('x', loc_x); + symbol_node.setAttribute('y', loc_y); + symbol_node.setAttribute('height', this.metadata['symbol_size'][symbol][0]); + symbol_node.setAttribute('width', this.metadata['symbol_size'][symbol][1]); + symbol_node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#' + this.__hashed_id(symbol)); + g_node.appendChild(symbol_node); + const orderLayer = this.__svg_element_from_id('HighestOrderLayer'); + if (!orderLayer) + throw new Error(`Unable to find svg order layer.`); + orderLayer.appendChild(g_node); + } + + clear() { + this.__set_note('', ''); + $(`#${this.__hashed_id('DislodgedUnitLayer')} use`).remove(); + $(`#${this.__hashed_id('UnitLayer')} use`).remove(); + $(`#${this.__hashed_id('SupplyCenterLayer')} use`).remove(); + $(`#${this.__hashed_id('Layer1')} g`).remove(); + $(`#${this.__hashed_id('Layer2')} g`).remove(); + $(`#${this.__hashed_id('HighestOrderLayer')} g`).remove(); + this.__restore_initial_influences(); + } + + render(includeOrders, orders) { + // Setting phase and note. + const nb_centers = []; + for (let power of Object.values(this.game.powers)) { + if (!power.isEliminated()) + nb_centers.push([power.name.substr(0, 3), power.centers.length]); + } + // Sort nb_centers by descending number of centers. + nb_centers.sort((a, b) => { + return -(a[1] - b[1]) || a[0].localeCompare(b[0]); + }); + const nb_centers_per_power = nb_centers.map((couple) => (couple[0] + ': ' + couple[1])).join(' '); + this.__set_current_phase(); + this.__set_note(nb_centers_per_power, this.game.note); + + // Adding units, supply centers, influence and orders. + const scs = new Set(this.mapData.supplyCenters); + for (let power of Object.values(this.game.powers)) { + for (let unit of power.units) + this.__add_unit(unit, power.name, false); + for (let unit of Object.keys(power.retreats)) + this.__add_unit(unit, power.name, true); + for (let center of power.centers) { + this.__add_supply_center(center, power.name); + this.__set_influence(center, power.name); + scs.delete(center); + } + if (!power.isEliminated()) { + for (let loc of power.influence) { + if (!this.mapData.supplyCenters.has(loc)) + this.__set_influence(loc, power.name); + } + } + + if (includeOrders) { + const powerOrders = (orders && orders.hasOwnProperty(power.name) && orders[power.name]) || []; + for (let order of powerOrders) { + const tokens = order.split(/ +/); + if (!tokens || tokens.length < 3) + continue; + const unit_loc = tokens[1]; + if (tokens[2] === 'H') + this.issueHoldOrder(unit_loc, power.name); + else if (tokens[2] === '-') { + const destLoc = tokens[tokens.length - (tokens[tokens.length - 1] === 'VIA' ? 2 : 1)]; + this.issueMoveOrder(unit_loc, destLoc, power.name); + } else if (tokens[2] === 'S') { + const destLoc = tokens[tokens.length - 1]; + if (tokens.includes('-')) { + const srcLoc = tokens[4]; + this.issueSupportMoveOrder(unit_loc, srcLoc, destLoc, power.name); + } else { + this.issueSupportHoldOrder(unit_loc, destLoc, power.name); + } + } else if (tokens[2] === 'C') { + const srcLoc = tokens[4]; + const destLoc = tokens[tokens.length - 1]; + if ((srcLoc !== destLoc) && (tokens.includes('-'))) { + this.issueConvoyOrder(unit_loc, srcLoc, destLoc, power.name); + } + } else if (tokens[2] === 'B') { + this.issueBuildOrder(tokens[0], unit_loc, power.name); + } else if (tokens[2] === 'D') { + this.issueDisbandOrder(unit_loc); + } else if (tokens[2] === 'R') { + const srcLoc = tokens[1]; + const destLoc = tokens[3]; + this.issueMoveOrder(srcLoc, destLoc, power.name); + } else { + throw new Error(`Unknown error to render (${order}).`); + } + } + } + } + // Adding remaining supply centers. + for (let remainingCenter of scs) + this.__add_supply_center(remainingCenter, null); + } + + update(game, mapData, showOrders, orders) { + this.game = game; + this.mapData = mapData; + this.clear(); + this.render(showOrders, orders); + } +} diff --git a/diplomacy/web/src/gui/diplomacy/utils/dipStorage.jsx b/diplomacy/web/src/gui/diplomacy/utils/dipStorage.jsx new file mode 100644 index 0000000..db5baad --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/utils/dipStorage.jsx @@ -0,0 +1,140 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/* DipStorage scheme: +global +- connection + - username + - hostname + - port + - showServerFields +users +- (username) + - games + - (game_id) + - phase: string + - local_orders: {power_name => [orders]} +*/ + +let STORAGE = null; + +export class DipStorage { + static load() { + if (!STORAGE) { + const global = window.localStorage.global; + const users = window.localStorage.users; + STORAGE = { + global: (global && JSON.parse(global)) || { + connection: { + username: null, + hostname: null, + port: null, + showServerFields: null + } + }, + users: (users && JSON.parse(users)) || {} + }; + } + } + + static save() { + if (STORAGE) { + window.localStorage.global = JSON.stringify(STORAGE.global); + window.localStorage.users = JSON.stringify(STORAGE.users); + } + } + + static getConnectionForm() { + DipStorage.load(); + return Object.assign({}, STORAGE.global.connection); + } + + static getUserGames(username) { + DipStorage.load(); + if (STORAGE.users[username]) + return Object.keys(STORAGE.users[username].games); + return null; + } + + static getUserGameOrders(username, gameID, gamePhase) { + DipStorage.load(); + if (STORAGE.users[username] && STORAGE.users[username].games[gameID] + && STORAGE.users[username].games[gameID].phase === gamePhase) + return Object.assign({}, STORAGE.users[username].games[gameID].local_orders); + return null; + } + + static setConnectionUsername(username) { + DipStorage.load(); + STORAGE.global.connection.username = username; + DipStorage.save(); + } + + static setConnectionHostname(hostname) { + DipStorage.load(); + STORAGE.global.connection.hostname = hostname; + DipStorage.save(); + } + + static setConnectionPort(port) { + DipStorage.load(); + STORAGE.global.connection.port = port; + DipStorage.save(); + } + + static setConnectionshowServerFields(showServerFields) { + DipStorage.load(); + STORAGE.global.connection.showServerFields = showServerFields; + DipStorage.save(); + } + + static addUserGame(username, gameID) { + DipStorage.load(); + if (!STORAGE.users[username]) + STORAGE.users[username] = {games: {}}; + if (!STORAGE.users[username].games[gameID]) + STORAGE.users[username].games[gameID] = {phase: null, local_orders: {}}; + DipStorage.save(); + } + + static addUserGameOrders(username, gameID, gamePhase, powerName, orders) { + DipStorage.addUserGame(username, gameID); + if (STORAGE.users[username].games[gameID].phase !== gamePhase) + STORAGE.users[username].games[gameID] = {phase: null, local_orders: {}}; + STORAGE.users[username].games[gameID].phase = gamePhase; + STORAGE.users[username].games[gameID].local_orders[powerName] = orders; + DipStorage.save(); + } + + static removeUserGame(username, gameID) { + DipStorage.load(); + if (STORAGE.users[username] && STORAGE.users[username].games[gameID]) { + delete STORAGE.users[username].games[gameID]; + DipStorage.save(); + } + } + + static clearUserGameOrders(username, gameID, powerName) { + DipStorage.addUserGame(username, gameID); + if (powerName) { + if (STORAGE.users[username].games[gameID].local_orders[powerName]) + delete STORAGE.users[username].games[gameID].local_orders[powerName]; + } else { + STORAGE.users[username].games[gameID] = {phase: null, local_orders: {}}; + } + DipStorage.save(); + } +} diff --git a/diplomacy/web/src/gui/diplomacy/utils/inline_game_view.jsx b/diplomacy/web/src/gui/diplomacy/utils/inline_game_view.jsx new file mode 100644 index 0000000..0ada4c9 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/utils/inline_game_view.jsx @@ -0,0 +1,129 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import {Button} from "../../core/widgets"; +import {JoinForm} from "../forms/join_form"; +import {STRINGS} from "../../../diplomacy/utils/strings"; + +export class InlineGameView { + constructor(page, gameData) { + this.page = page; + this.game = gameData; + this.get = this.get.bind(this); + this.joinGame = this.joinGame.bind(this); + this.showGame = this.showGame.bind(this); + } + + joinGame(formData) { + const form = { + power_name: formData[`power_name_${this.game.game_id}`], + registration_password: formData[`registration_password_${this.game.game_id}`] + }; + if (!form.power_name) + form.power_name = null; + if (!form.registration_password) + form.registration_password = null; + form.game_id = this.game.game_id; + this.page.channel.joinGame(form) + .then((networkGame) => { + this.game = networkGame.local; + this.page.addToMyGames(this.game); + return networkGame.getAllPossibleOrders(); + }) + .then(allPossibleOrders => { + this.game.setPossibleOrders(allPossibleOrders); + this.page.loadGame(this.game, {success: 'Game joined.'}); + }) + .catch((error) => { + this.page.error('Error when joining game ' + this.game.game_id + ': ' + error); + }); + } + + showGame() { + this.page.loadGame(this.game); + } + + getJoinUI() { + if (this.game.role) { + // Game already joined. + return ( + <div className={'games-form'}> + <Button key={'button-show-' + this.game.game_id} title={'show'} onClick={this.showGame}/> + <Button key={'button-leave-' + this.game.game_id} title={'leave'} + onClick={() => this.page.leaveGame(this.game.game_id)}/> + </div> + ); + } else { + // Game not yet joined. + return <JoinForm key={this.game.game_id} game_id={this.game.game_id} powers={this.game.controlled_powers} + onSubmit={this.joinGame}/>; + } + } + + getMyGamesButton() { + if (this.page.hasMyGame(this.game.game_id)) { + if (!this.game.client) { + // Game in My Games and not joined. We can remove it. + return <Button key={`my-game-remove`} title={'Remove from My Games'} + onClick={() => this.page.removeFromMyGames(this.game.game_id)}/>; + } + } else { + // Game not in My Games, we can add it. + return <Button key={`my-game-add`} title={'Add to My Games'} + onClick={() => this.page.addToMyGames(this.game)}/>; + } + return ''; + } + + get(name) { + if (name === 'players') { + return `${this.game.n_players} / ${this.game.n_controls}`; + } + if (name === 'rights') { + const elements = []; + if (this.game.observer_level) { + let levelName = ''; + if (this.game.observer_level === STRINGS.MASTER_TYPE) + levelName = 'master'; + else if (this.game.observer_level === STRINGS.OMNISCIENT_TYPE) + levelName = 'omniscient'; + else + levelName = 'observer'; + elements.push((<p key={0}><strong>Observer right:</strong><br/>{levelName}</p>)); + } + if (this.game.controlled_powers && this.game.controlled_powers.length) { + const powers = this.game.controlled_powers.slice(); + powers.sort(); + elements.push(( + <div key={1}><strong>Currently handled power{powers.length === 1 ? '' : 's'}</strong></div>)); + for (let power of powers) + elements.push((<div key={power}>{power}</div>)); + } + return elements.length ? (<div>{elements}</div>) : ''; + } + if (name === 'rules') { + if (this.game.rules) + return <div>{this.game.rules.map(rule => <div key={rule}>{rule}</div>)}</div>; + return ''; + } + if (name === 'join') + return this.getJoinUI(); + if (name === 'my_games') + return this.getMyGamesButton(); + return this.game[name]; + } +} diff --git a/diplomacy/web/src/gui/diplomacy/utils/map_data.js b/diplomacy/web/src/gui/diplomacy/utils/map_data.js new file mode 100644 index 0000000..73d5338 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/utils/map_data.js @@ -0,0 +1,98 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {Province} from "./province"; + +export class MapData { + constructor(mapInfo, game) { + // mapInfo: {powers: [], supply_centers: [], aliases: {alias: name}, loc_type: {loc => type}, loc_abut: {loc => [abuts]}} + // game: a NetworkGame object. + this.game = game; + this.powers = new Set(mapInfo.powers); + this.supplyCenters = new Set(mapInfo.supply_centers); + this.aliases = Object.assign({}, mapInfo.aliases); + this.provinces = {}; + for (let entry of Object.entries(mapInfo.loc_type)) { + const provinceName = entry[0]; + const provinceType = entry[1]; + this.provinces[provinceName] = new Province(provinceName, provinceType, this.supplyCenters.has(provinceName)); + } + for (let entry of Object.entries(mapInfo.loc_abut)) { + this.getProvince(entry[0]).setNeighbors(entry[1].map(name => this.getProvince(name))); + } + for (let province of Object.values(this.provinces)) { + province.setCoasts(this.provinces); + } + for (let power of Object.values(this.game.powers)) { + for (let center of power.centers) { + this.getProvince(center).setController(power.name, 'C'); + } + for (let loc of power.influence) { + this.getProvince(loc).setController(power.name, 'I'); + } + for (let unit of power.units) { + this.__add_unit(unit, power.name); + } + for (let unit of Object.keys(power.retreats)) { + this.__add_retreat(unit, power.name); + } + } + for (let entry of Object.entries(this.aliases)) { + const alias = entry[0]; + const provinceName = entry[1]; + const province = this.getProvince(provinceName); + if (province) + province.aliases.push(alias); + } + } + + __add_unit(unit, power_name) { + const splitUnit = unit.split(/ +/); + const unitType = splitUnit[0]; + const location = splitUnit[1]; + const province = this.getProvince(location); + province.setController(power_name, 'U'); + province.unit = unitType; + } + + __add_retreat(unit, power_name) { + const splitUnit = unit.split(/ +/); + const location = splitUnit[1]; + const province = this.getProvince(location); + province.retreatController = power_name; + province.retreatUnit = unit; + } + + getProvince(abbr) { + if (abbr === '') + return null; + if (abbr[0] === '_') + abbr = abbr.substr(1, 3); + if (!abbr) + return null; + if (!this.provinces.hasOwnProperty(abbr)) { + const firstLetter = abbr[0]; + if (firstLetter === firstLetter.toLowerCase()) { + abbr = abbr.toUpperCase(); + } else { + abbr = abbr.toLowerCase(); + } + } + if (!this.provinces.hasOwnProperty(abbr)) + abbr = this.aliases[abbr]; + return this.provinces[abbr]; + } +} diff --git a/diplomacy/web/src/gui/diplomacy/utils/order.js b/diplomacy/web/src/gui/diplomacy/utils/order.js new file mode 100644 index 0000000..e314b9f --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/utils/order.js @@ -0,0 +1,24 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +export class Order { + constructor(orderString, isLocal) { + const pieces = orderString.split(/ +/); + this.loc = pieces[1]; + this.order = orderString; + this.local = Boolean(isLocal); + } +} diff --git a/diplomacy/web/src/gui/diplomacy/utils/order_building.js b/diplomacy/web/src/gui/diplomacy/utils/order_building.js new file mode 100644 index 0000000..3758898 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/utils/order_building.js @@ -0,0 +1,211 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +/*eslint no-unused-vars: ["error", { "args": "none" }]*/ + +function assertLength(expected, given) { + if (expected !== given) + throw new Error(`Length error: expected ${expected}, given ${given}.`); +} + +export class ProvinceCheck { + + static retreated(province, powerName) { + const retreatProvince = province.getRetreated(powerName); + if (!retreatProvince) + throw new Error(`No retreated location at province ${province.name}.`); + // No confusion possible, we select the only occupied location at this province. + return [retreatProvince.retreatUnit]; + } + + static present(province, powerName) { + let unit = null; + let presenceProvince = province.getOccupied(powerName); + if (presenceProvince) { + unit = `${presenceProvince.unit} ${presenceProvince.name}`; + } else { + presenceProvince = province.getRetreated(powerName); + if (!presenceProvince) + throw new Error(`No unit or retreat at province ${province.name}.`); + unit = presenceProvince.retreatUnit; + } + return [unit]; + } + + static occupied(province, powerName) { + const occupiedProvince = province.getOccupied(powerName); + if (!occupiedProvince) + throw new Error(`No occupied location at province ${province.name}.`); + // No confusion possible, we select the only occupied location at this province. + const unit = occupiedProvince.unit; + const name = occupiedProvince.name.toUpperCase(); + return [`${unit} ${name}`]; + } + + static occupiedByAny(province, unusedPowerName) { + return ProvinceCheck.occupied(province, null); + } + + static any(province, unusedPowerName) { + // There may be many locations available for a province (e.g. many coasts). + return province.getLocationNames(); + } + + static buildOrder(path) { + switch (path[0]) { + case 'H': + return ProvinceCheck.holdToString(path); + case 'M': + return ProvinceCheck.moveToString(path); + case 'V': + return ProvinceCheck.moveViaToString(path); + case 'S': + return ProvinceCheck.supportToString(path); + case 'C': + return ProvinceCheck.convoyToString(path); + case 'R': + return ProvinceCheck.retreatToString(path); + case 'D': + return ProvinceCheck.disbandToString(path); + case 'A': + return ProvinceCheck.buildArmyToString(path); + case 'F': + return ProvinceCheck.buildFleetToString(path); + default: + throw new Error('Unable to build order from path ' + JSON.stringify(path)); + } + } + + static holdToString(path) { + assertLength(2, path.length); + return `${path[1]} ${path[0]}`; + } + + static moveToString(path) { + assertLength(3, path.length); + return `${path[1]} - ${path[2]}`; + } + + static moveViaToString(path) { + return ProvinceCheck.moveToString(path) + ' VIA'; + } + + static supportToString(path) { + assertLength(4, path.length); + let order = `${path[1]} ${path[0]} ${path[2]}`; + if (path[2].substr(2) !== path[3]) + order += ` - ${path[3]}`; + return order; + } + + static convoyToString(path) { + assertLength(4, path.length); + return `${path[1]} ${path[0]} ${path[2]} - ${path[3]}`; + } + + static retreatToString(path) { + assertLength(3, path.length); + return `${path[1]} ${path[0]} ${path[2]}`; + } + + static disbandToString(path) { + assertLength(2, path.length); + return `${path[1]} ${path[0]}`; + } + + static buildArmyToString(path) { + assertLength(2, path.length); + return `${path[0]} ${path[1]} B`; + } + + static buildFleetToString(path) { + assertLength(2, path.length); + return `${path[0]} ${path[1]} B`; + } + +} + +export const ORDER_BUILDER = { + H: { + name: 'hold (H)', + steps: [ProvinceCheck.occupied] + }, + M: { + name: 'move (M)', + steps: [ProvinceCheck.occupied, ProvinceCheck.any] + }, + V: { + name: 'move VIA (V)', + steps: [ProvinceCheck.occupied, ProvinceCheck.any] + }, + S: { + name: 'support (S)', + steps: [ProvinceCheck.occupied, ProvinceCheck.occupiedByAny, ProvinceCheck.any] + }, + C: { + name: 'convoy (C)', + steps: [ProvinceCheck.occupied, ProvinceCheck.occupiedByAny, ProvinceCheck.any] + }, + R: { + name: 'retreat (R)', + steps: [ProvinceCheck.retreated, ProvinceCheck.any] + }, + D: { + name: 'disband (D)', + steps: [ProvinceCheck.present] + }, + A: { + name: 'build army (A)', + steps: [ProvinceCheck.any] + }, + F: { + name: 'build fleet (F)', + steps: [ProvinceCheck.any] + }, +}; + +export const POSSIBLE_ORDERS = { + // Allowed orders for movement phase step. + M: ['H', 'M', 'V', 'S', 'C'], + // Allowed orders for retreat phase step. + R: ['R', 'D'], + // Allowed orders for adjustment phase step. + A: ['D', 'A', 'F'], + sorting: { + M: {M: 0, V: 1, S: 2, C: 3, H: 4}, + R: {R: 0, D: 1}, + A: {A: 0, F: 1, D: 2} + }, + sortOrderTypes: function (arr, phaseType) { + arr.sort((a, b) => POSSIBLE_ORDERS.sorting[phaseType][a] - POSSIBLE_ORDERS.sorting[phaseType][b]); + } +}; + +export function extendOrderBuilding(powerName, orderType, currentOrderPath, location, onBuilding, onBuilt, onError) { + const selectedPath = [orderType].concat(currentOrderPath, location); + if (selectedPath.length - 1 < ORDER_BUILDER[orderType].steps.length) { + // Checker OK, update. + onBuilding(powerName, selectedPath); + } else { + try { + // Order created. + const orderString = ProvinceCheck.buildOrder(selectedPath); + onBuilt(powerName, orderString); + } catch (error) { + onError(error.toString()); + } + } +} diff --git a/diplomacy/web/src/gui/diplomacy/utils/power_view.jsx b/diplomacy/web/src/gui/diplomacy/utils/power_view.jsx new file mode 100644 index 0000000..1796b1b --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/utils/power_view.jsx @@ -0,0 +1,59 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import {STRINGS} from "../../../diplomacy/utils/strings"; +import React from "react"; + +function getName(power) { + if (power.isEliminated()) + return <em><s>{power.name.toLowerCase()}</s> (eliminated)</em>; + return power.name; +} + +function getController(power) { + const controller = power.getController(); + return <span className={controller === STRINGS.DUMMY ? 'dummy' : 'controller'}>{controller}</span>; +} + +function getOrderFlag(power) { + const value = ['no', 'empty', 'yes'][power.order_is_set]; + return <span className={value}>{value}</span>; +} + +function getWaitFlag(power) { + return <span className={power.wait ? 'wait' : 'no-wait'}>{power.wait ? 'yes' : 'no'}</span>; +} + +const GETTERS = { + name: getName, + controller: getController, + order_is_set: getOrderFlag, + wait: getWaitFlag +}; + +export class PowerView { + constructor(power) { + this.power = power; + } + + static wrap(power) { + return new PowerView(power); + } + + get(key) { + return GETTERS[key](this.power); + } +} diff --git a/diplomacy/web/src/gui/diplomacy/utils/province.js b/diplomacy/web/src/gui/diplomacy/utils/province.js new file mode 100644 index 0000000..fc48ac7 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/utils/province.js @@ -0,0 +1,117 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +const ProvinceType = { + WATER: 'WATER', + COAST: 'COAST', + PORT: 'PORT', + LAND: 'LAND' +}; + +export class Province { + constructor(name, type, isSupplyCenter) { + this.name = name; + this.type = type; + this.coasts = {}; + this.parent = null; + this.neighbors = {}; + this.isSupplyCenter = isSupplyCenter; + this.controller = null; // null or power name. + this.controlType = null; // null, C (center), I (influence) or U (unit). + this.unit = null; // null, A or F + this.retreatController = null; + this.retreatUnit = null; // null or `{unit type} {loc}` + this.aliases = []; + } + + compareControlType(a, b) { + const controlTypeLevels = {C: 0, I: 1, U: 2}; + return controlTypeLevels[a] - controlTypeLevels[b]; + } + + __set_controller(controller, controlType) { + this.controller = controller; + this.controlType = controlType; + for (let coast of Object.values(this.coasts)) + coast.setController(controller, controlType); + } + + setController(controller, controlType) { + if (!['C', 'I', 'U'].includes(controlType)) + throw new Error(`Invalid province control type (${controlType}), expected 'C', 'I' or 'U'.`); + if (this.controller) { + const controlTypeComparison = this.compareControlType(controlType, this.controlType); + if (controlTypeComparison === 0) + throw new Error(`Found 2 powers trying to control same province (${this.name}) with same ` + + `control type (${controlType} VS ${this.controlType}).`); + if (controlTypeComparison > 0) + this.__set_controller(controller, controlType); + } else + this.__set_controller(controller, controlType); + } + + setCoasts(provinces) { + const name = this.name.toUpperCase(); + for (let entry of Object.entries(provinces)) { + const pieces = entry[0].split(/[^A-Za-z0-9]+/); + if (pieces.length > 1 && pieces[0].toUpperCase() === name) { + this.coasts[entry[0]] = entry[1]; + entry[1].parent = this; + } + } + } + + setNeighbors(neighborProvinces) { + for (let province of neighborProvinces) + this.neighbors[province.name] = province; + } + + getLocationNames() { + const arr = Object.keys(this.coasts); + arr.splice(0, 0, this.name); + return arr; + } + + getOccupied(powerName) { + if (!this.controller) + return null; + if (powerName && this.controller !== powerName) + return null; + if (this.unit) + return this; + for (let coast of Object.values(this.coasts)) + if (coast.unit) + return coast; + return null; + } + + getRetreated(powerName) { + if (this.retreatController === powerName) + return this; + for (let coast of Object.values(this.coasts)) + if (coast.retreatController === powerName) + return coast; + return null; + } + + isCoast() { + return this.type === ProvinceType.COAST; + } + + isWater() { + return this.type === ProvinceType.WATER; + } +} diff --git a/diplomacy/web/src/gui/diplomacy/widgets/message_view.jsx b/diplomacy/web/src/gui/diplomacy/widgets/message_view.jsx new file mode 100644 index 0000000..045a108 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/widgets/message_view.jsx @@ -0,0 +1,57 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import {UTILS} from "../../../diplomacy/utils/utils"; +import PropTypes from 'prop-types'; + +export class MessageView extends React.Component { + // message + render() { + const message = this.props.message; + const owner = this.props.owner; + const id = this.props.id ? {id: this.props.id} : {}; + const messagesLines = message.message.replace('\r\n', '\n').replace('\r', '\n').split('\n'); + let onClick = null; + const classNames = ['game-message']; + if (owner === message.sender) + classNames.push('message-sender'); + else { + classNames.push('message-recipient'); + if (message.read || this.props.read) + classNames.push('message-read'); + onClick = this.props.onClick ? {onClick: () => this.props.onClick(message)} : {}; + } + return ( + <div className={'game-message-wrapper'} {...id}> + <div className={classNames.join(' ')} {...onClick}> + <div className={'message-header'}> + {message.sender} {UTILS.html.UNICODE_SMALL_RIGHT_ARROW} {message.recipient} + </div> + <div className={'message-content'}>{messagesLines.map((line, lineIndex) => <div key={lineIndex}>{line}</div>)}</div> + </div> + </div> + ); + } +} + +MessageView.propTypes = { + message: PropTypes.object, + owner: PropTypes.string, + onClick: PropTypes.func, + id: PropTypes.string, + read: PropTypes.bool +}; diff --git a/diplomacy/web/src/gui/diplomacy/widgets/power_order.jsx b/diplomacy/web/src/gui/diplomacy/widgets/power_order.jsx new file mode 100644 index 0000000..28a5421 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/widgets/power_order.jsx @@ -0,0 +1,79 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from "react"; +import {Button} from "../../core/widgets"; +import PropTypes from 'prop-types'; + +export class PowerOrder extends React.Component { + render() { + const orderEntries = this.props.orders ? Object.entries(this.props.orders) : null; + let display = null; + if (orderEntries) { + if (orderEntries.length) { + orderEntries.sort((a, b) => a[1].order.localeCompare(b[1].order)); + display = ( + <div className={'container order-list'}> + {orderEntries.map((entry, index) => ( + <div + className={`row order-entry entry-${1 + index % 2} ` + (entry[1].local ? 'local' : 'server')} + key={index}> + <div className={'col align-self-center order'}> + <span className={'order-string'}>{entry[1].order}</span> + {entry[1].local ? '' : <span className={'order-mark'}> [S]</span>} + </div> + <div className={'col remove-button'}> + <Button title={'-'} onClick={() => this.props.onRemove(this.props.name, entry[1])}/> + </div> + </div> + ))} + </div> + ); + } else if (this.props.serverCount === 0) { + display = (<div className={'empty-orders'}>Empty orders set</div>); + } else { + display = (<div className={'empty-orders'}>Local empty orders set</div>); + } + } else { + if (this.props.serverCount < 0) { + display = <div className={'no-orders'}>No orders!</div>; + } else if (this.props.serverCount === 0) { + display = <div className={'empty-orders'}>Empty orders set</div>; + } else { + display = (<div className={'empty-orders'}>Local empty orders set</div>); + } + } + return ( + <div className={'power-orders'}> + <div className={'title'}> + <span className={'name'}>{this.props.name}</span> + <span className={this.props.wait ? 'wait' : 'no-wait'}> + {(this.props.wait ? ' ' : ' not') + ' waiting'} + </span> + </div> + {display} + </div> + ); + } +} + +PowerOrder.propTypes = { + wait: PropTypes.bool, + name: PropTypes.string, + orders: PropTypes.object, + serverCount: PropTypes.number, + onRemove: PropTypes.func, +}; diff --git a/diplomacy/web/src/index.css b/diplomacy/web/src/index.css new file mode 100644 index 0000000..f33b116 --- /dev/null +++ b/diplomacy/web/src/index.css @@ -0,0 +1,401 @@ +/** Bootstrap. **/ +/** Common. **/ + +a.dropdown-item { + cursor: pointer; +} + +.top-msg { + border-bottom: 1px solid black; + font-weight: bold; + text-align: center; + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1; + background-color: white; +} + +.top-msg .msg { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.top-msg .with-msg { + cursor: pointer; +} + +.top-msg .no-msg { + text-overflow: initial; +} + +.top-msg .msg + .msg { + border-left: 1px solid black; +} + +.top-msg .error { + color: red; +} + +.top-msg .info { + color: blue; +} + +.top-msg .success { + color: green; +} + +#past-map svg, #current-map svg { + display: block; + width: 100%; + height: auto; +} + +.history-current-orders { + background-color: red; + color: white; + font-weight: bold; + position: absolute; + top: 0; + right: 0; + border: 4px solid black; + margin: 5px 24px; + padding: 5px 10px 5px 10px; +} + +table caption { + caption-side: top; +} + +main { + padding: 10px; +} + +.power-actions-form button + button, .panel-orders .bar button + button, .games-form button + button { + margin-left: 0.5rem; +} + +.link-unread-message { + display: block; + text-align: center; + font-weight: bold; + text-decoration: underline; + margin-bottom: 1rem; +} + +.link-unread-message:hover { + text-decoration: none; +} + +.power-name { + font-weight: bold; + color: red; + text-align: center; +} + +#current-power { + color: red; + font-weight: bold; + text-align: center; +} + +.page-messages { + position: fixed; + bottom: 0; + right: 0; + z-index: 1; + width: 30%; +} + +.page-message { + border: 8px solid; + padding: 10px; + cursor: pointer; + font-size: 1.3rem; + font-weight: bold; +} + +.page-message.error { + border-color: rgb(245, 205, 205); + background-color: rgba(255, 215, 215, 0.9); + color: rgb(200, 120, 120); +} + +.page-message.info { + border-color: rgb(245, 245, 205); + background-color: rgba(255, 255, 215, 0.9); + color: rgb(200, 200, 120); +} + +.page-message.success { + border-color: rgb(205, 245, 205); + background-color: rgba(215, 255, 215, 0.9); + color: rgb(120, 200, 120); +} + +.order-result { + font-weight: bold; +} + +.order-result .bounce { + color: red; +} + +.order-result .success { + color: green; +} + +.past-orders { + font-size: 0.9rem; + max-height: 500px; + overflow: auto; +} + +.past-orders .row:nth-child(odd) { + background-color: rgb(244, 244, 245); +} + +.past-orders .row .past-power-name, .past-orders .row .past-power-orders { + padding: 5px; +} + +.past-orders .row .past-power-name { + font-weight: bold; +} + +.past-orders .row .past-power-orders > div:before { + content: "\2192"; + margin-right: 1em; +} + +.bar > * { + display: inline-block; +} + +.page > .title { + border-bottom: 1px solid silver; + padding: 10px; +} + +.left { + float: left; +} + +.right { + float: right; +} + +.action { + cursor: pointer; +} + +.action .updated { + font-weight: bold; +} + +.action .update { + font-size: 0.8em; + color: white; + background-color: red; + display: inline-block; + padding: 0 0.4em 0 0.4em; + margin-left: 0.4em; + border: 1px solid lightcoral; +} + +.orders, .game-messages { + max-height: 500px; + overflow: auto; + border: 1px solid silver; + padding: 10px; +} + +.panel-orders > .bar { + border-top: 1px solid silver; + border-left: 1px solid silver; + border-right: 1px solid silver; +} + +.panel-orders .summary { + border-left: 1px solid silver; + border-right: 1px solid silver; +} + +.panel-orders .summary > span + span:before { + content: ', '; +} + +.fancy-wrapper { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + padding-top: 10%; + background-color: rgba(100, 100, 110, 0.5); +} + +.fancy-box { + border: 1px solid silver; + position: relative; + margin: auto; +} + +.fancy-box .fancy-bar { + background-color: rgb(240, 240, 240); + border-bottom: 1px solid silver; + padding-top: 10px; + padding-bottom: 10px; +} + +.fancy-box .fancy-button { + text-align: right; +} + +.fancy-box .fancy-content { + padding-top: 10px; + padding-bottom: 10px; + background-color: white; +} + +.power-orders { + padding-bottom: 8px; +} + +.power-orders .no-orders, .empty-orders { + text-align: center; + font-weight: bold; + font-style: italic; +} + +.power-orders .no-orders { + color: lightgray; +} + +.power-orders .empty-orders { + color: gray; +} + +.power-orders .title { + background-color: rgb(230, 230, 235); + padding: 10px; + margin-bottom: 2px; +} + +.power-orders .title .name { + font-weight: bold; +} + +.dummy, .neutral { + font-style: italic; + color: gray; +} + +.controller { + font-weight: bold; +} + +.power-orders .title .wait, .wait, .no { + font-weight: bold; + color: red; +} + +.power-orders .title .no-wait, .no-wait, .yes { + font-weight: bold; + color: green; +} + +.empty { + font-weight: bold; + color: orange; +} + +.power-orders .title button { + font-family: "Courier New", Courier, monospace; +} + +.power-orders .order-entry { + padding-top: 2px; + padding-bottom: 2px; +} + +.power-orders .order-entry.server .order-string { + text-decoration: underline; +} + +.power-orders .order-entry.server .order-mark { + color: red; +} + +.power-orders .entry-2 { + background-color: rgb(248, 248, 248); +} + +.power-orders .order { + font-weight: bold; +} + +.power-orders .remove-button { + text-align: right; +} + +.game-message { + padding: 10px; + width: 75%; + border-width: 4px; + border-style: solid; + border-radius: 10px; +} + +.game-message .message-header { + font-weight: bold; +} + +.game-message.message-recipient { + float: left; + border-color: rgb(240, 200, 200); + background-color: rgb(255, 220, 220); + cursor: pointer; +} + +.game-message.message-recipient.message-read { + border-color: rgb(200, 240, 200); + background-color: rgb(220, 255, 220); + cursor: default; +} + +.game-message.message-sender { + float: right; + border-color: rgb(200, 200, 240); + background-color: rgb(220, 220, 255); +} + +.game-message-wrapper { + overflow: auto; + clear: both; +} + +.game-message-wrapper + .game-message-wrapper { + margin-top: 10px; +} + +.button-server { + border: 1px solid silver; + border-radius: 4px; + padding: 5px; + cursor: pointer; + margin-bottom: 10px; +} + +.button-server:hover { + background-color: rgb(245, 245, 245); +} + +.button-server:active { + background-color: rgb(230, 230, 230); +} + +/** Page login. **/ +/** Page games. **/ +/** Page game. **/ diff --git a/diplomacy/web/src/index.js b/diplomacy/web/src/index.js new file mode 100644 index 0000000..d074bce --- /dev/null +++ b/diplomacy/web/src/index.js @@ -0,0 +1,28 @@ +// ============================================================================== +// 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/>. +// ============================================================================== +import React from 'react'; +import ReactDOM from 'react-dom'; +import {Page} from "./gui/core/page"; +import 'popper.js'; +import 'bootstrap/dist/js/bootstrap'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import './index.css'; + + +// ======================================== + +ReactDOM.render(<Page/>, document.getElementById('root')); diff --git a/diplomacy/web/src/standard.svg b/diplomacy/web/src/standard.svg new file mode 120000 index 0000000..abb544e --- /dev/null +++ b/diplomacy/web/src/standard.svg @@ -0,0 +1 @@ +../../maps/svg/standard.svg
\ No newline at end of file |