Skip to content

Commit

Permalink
WebDip - Implemented a gunboat sampler for better openings
Browse files Browse the repository at this point in the history
  • Loading branch information
ppaquette committed Sep 18, 2019
1 parent 3c7ed06 commit 6f6b4bd
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 9 deletions.
74 changes: 65 additions & 9 deletions diplomacy_research/scripts/launch_bot_webdip.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,28 @@
from diplomacy.utils import exceptions
from tornado import gen, ioloop, util
from diplomacy_research.players.benchmark_player import WebDiplomacyPlayer
from diplomacy_research.utils.openings import get_orders_from_gunboat_openings

# Constants
LOGGER = logging.getLogger('diplomacy_research.scripts.launch_bot_webdip')
PERIOD_SECONDS = 5
PERIOD_ERRORS = 300
MAX_ERRORS = 5
NB_MODELS = 2

class GunboatSamplingPlayer():
""" Mock player that samples orders according to a distribution """

@staticmethod
@gen.coroutine
def get_orders(_, power_name):
""" Samples according to the gunboat opening orders distribution """
return get_orders_from_gunboat_openings(power_name)


class WebDipBot():
""" WebDipBot class """
__slots__ = ['period_seconds', 'players', 'max_batch_size', 'api_cd', 'api_users', 'errors']
__slots__ = ['period_seconds', 'players', 'max_batch_size', 'api_cd', 'api_users', 'errors', 'known_game_ids']

def __init__(self, *, period_seconds=PERIOD_SECONDS, max_batch_size=32):
""" Initialize the bot.
Expand All @@ -48,6 +59,7 @@ def __init__(self, *, period_seconds=PERIOD_SECONDS, max_batch_size=32):
self.api_cd = [] # Keys stored in API_KEY_CD_01 to API_KEY_CD_20
self.api_users = [] # Keys stored in API_KEY_USER_01 to API_KEY_USER_20
self.errors = {} # {'game_id/country_id': [timestamps]}
self.known_game_ids = set() # Set of game ids where we already printed the assignments

# Loading API keys
for key_ix in range(1, 21):
Expand All @@ -70,8 +82,12 @@ def run(self):
'beam_0.50': WebDiplomacyPlayer(temperature=0.50, use_beam=True),
'beam_0.25': WebDiplomacyPlayer(temperature=0.25, use_beam=True),
'beam_0.10': WebDiplomacyPlayer(temperature=0.10, use_beam=True),
'greedy': WebDiplomacyPlayer(temperature=0.10, use_beam=False)}
yield self.players['greedy'].check_openings()
'greedy_1.00': WebDiplomacyPlayer(temperature=1.00, use_beam=False),
'greedy_0.50': WebDiplomacyPlayer(temperature=0.50, use_beam=False),
'greedy_0.25': WebDiplomacyPlayer(temperature=0.25, use_beam=False),
'greedy_0.10': WebDiplomacyPlayer(temperature=0.10, use_beam=False),
'gunboat_sampler': GunboatSamplingPlayer()}
yield self.players['greedy_0.10'].check_openings()

# List of all APIs
all_apis = self.api_cd + self.api_users
Expand Down Expand Up @@ -127,8 +143,15 @@ def submit_orders(self, api, game_id, country_id):
self.add_error(game_id, country_id)
return

# Querying the assignments
models = self.get_model_per_power(game)
current_phase = game.get_current_phase()
if game.game_id not in self.known_game_ids and game.map_name == 'standard' and current_phase == 'S1901M':
LOGGER.info('[%s] Models per power are: %s', game_id, models)
self.known_game_ids.add(game_id)

# Querying the model for object
player = self.get_player(game, power_name)
player = self.get_player(game, power_name, models)
if player is None:
LOGGER.error('Unable to retrieve the player for map %s.', game.map_name)
return
Expand All @@ -140,29 +163,62 @@ def submit_orders(self, api, game_id, country_id):
if not success:
self.add_error(game_id, country_id)

def get_player(self, game, power_name):
@staticmethod
def get_model_per_power(game, nb_models=NB_MODELS):
""" Computes a dictionary of model version per power
:param game: The current game object
:param nb_models: The number of models
:type game: diplomacy.Game
:return: A dictionary of {power_name: model_id}
"""
game_id = int(game.game_id) if str(game.game_id).isdigit() else 0
assignments = (3079 * game_id) % (nb_models ** 7) # 3079 is a prime for even hashing
models = {}

# Decode the assignments (nb_models ** nb_players)
# Each player has the same probability of being assigned to each model
# The assignment will always be the same for a given game_id / power_name
for power_name in game.powers:
models[power_name] = (assignments % nb_models)
assignments = assignments // nb_models
return models

def get_player(self, game, power_name, models):
""" Returns the player to query the orders
:param game: A game instance
:param power_name: The name of the power we are playing
:param models: A dictionary of {power_name: model_id}
:type game: diplomacy.Game
:return: A player object to query the orders
"""
# pylint: disable=too-many-return-statements
# Standard map
if game.map_name == 'standard':
if game.get_current_phase() in ('S1901M', 'F1901M'): # To get a diverse set of openings

# v1 - Uses the new gunboat empirical openings
if models.get(power_name, 0) == 1:
if game.get_current_phase() == 'S1901M': # Uses the empirical openings
return self.players['gunboat_sampler']
if game.get_current_phase() == 'F1901M': # Beam to get diverse second moves
return self.players['beam_0.25']
if self.game_stuck_in_local_optimum(game, power_name): # In case the bot gets stuck
return self.players['greedy_1.00']
return self.players['greedy_0.50'] # Greedy by default

# v0 - Default model
if game.get_current_phase() in ('S1901M', 'F1901M'): # To get a diverse set of openings
return self.players['beam_0.50']
if self.game_stuck_in_local_optimum(game, power_name): # In case the bot gets stuck
if self.game_stuck_in_local_optimum(game, power_name): # In case the bot gets stuck
return self.players['beam_0.50']
return self.players['greedy'] # Greedy by default
return self.players['greedy_0.10'] # Greedy by default

# 1v1 map
if game.map_name in ('standard_france_austria', 'standard_germany_italy'):
if game.get_current_phase() in ('S1901M', 'F1901M'): # To get a diverse set of openings
return self.players['beam_0.50']
if game.get_current_phase() in ('S1902M', 'F1902M'): # To get a diverse set of beginning/mid-game
return self.players['beam_0.10']
return self.players['greedy'] # Greedy by default
return self.players['greedy_0.10'] # Greedy by default

# Unsupported maps
return None
Expand Down
93 changes: 93 additions & 0 deletions diplomacy_research/utils/openings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
""" Openings
- Contains a list of standard openings for each power
"""
from numpy.random import choice

def get_standard_openings(power_name):
""" Returns a list of standard openings for a given power"""
return {
Expand Down Expand Up @@ -140,3 +142,94 @@ def get_standard_openings(power_name):
['F ANK - BLA', 'A CON - BUL', 'A SMY H'],
['F ANK - ARM', 'A CON - BUL', 'A SMY - CON'])
}.get(power_name, ())

def get_orders_from_gunboat_openings(power_name):
""" Samples openings orders from the gunboat openings
:param power_name: The name of the power we are playing
:return: A list of opening orders for that power
"""
gunboat_openings = {'AUSTRIA': {('A VIE - GAL', 'F TRI - ALB', 'A BUD - SER'): 6868,
('A VIE - TRI', 'F TRI - ALB', 'A BUD - SER'): 3033,
('A VIE - BUD', 'F TRI - ALB', 'A BUD - SER'): 1153,
('A VIE - GAL', 'F TRI - VEN', 'A BUD - SER'): 1969,
('A VIE - TYR', 'F TRI - VEN', 'A BUD - GAL'): 1615,
('A VIE - TYR', 'F TRI - ALB', 'A BUD - GAL'): 1513,
('A VIE - TYR', 'F TRI - ALB', 'A BUD - TRI'): 976,
('A VIE - GAL', 'F TRI H', 'A BUD - SER', ): 741,
('A VIE - TYR', 'F TRI - VEN', 'A BUD - RUM'): 535,
('A VIE - GAL', 'F TRI S A VEN', 'A BUD - SER'): 511},

'ENGLAND': {('F EDI - NWG', 'F LON - NTH', 'A LVP - YOR'): 6685,
('F EDI - NWG', 'F LON - NTH', 'A LVP - EDI'): 5647,
('F EDI - NTH', 'F LON - ENG', 'A LVP - YOR'): 4683,
('F EDI - NTH', 'F LON - ENG', 'A LVP - WAL'): 1744,
('F EDI - NTH', 'F LON - ENG', 'A LVP H'): 85,
('F EDI - NTH', 'F LON - ENG', 'A LVP - EDI'): 350,
('F EDI - NWG', 'F LON - NTH', 'A LVP - WAL'): 314,
('F EDI - NWG', 'F LON - ENG', 'A LVP - WAL'): 114,
('F EDI - NWG', 'F LON - ENG', 'A LVP - EDI'): 159,
('F EDI - NWG', 'F LON - ENG', 'A LVP - YOR'): 126},

'FRANCE': {('F BRE - MAO', 'A PAR - BUR', 'A MAR - SPA'): 4071,
('F BRE - MAO', 'A PAR - PIC', 'A MAR - SPA'): 1757,
('F BRE - MAO', 'A PAR - BUR', 'A MAR S A PAR - BUR'): 5298,
('F BRE - ENG', 'A PAR - PIC', 'A MAR - SPA'): 1128,
('F BRE - MAO', 'A PAR - PIC', 'A MAR - BUR'): 2277,
('F BRE - ENG', 'A PAR - BUR', 'A MAR S A PAR - BUR'): 1163,
('F BRE - MAO', 'A PAR - GAS', 'A MAR - BUR'): 939,
('F BRE - MAO', 'A PAR - BUR', 'A MAR - PIE'): 1029,
('F BRE - ENG', 'A PAR - PIC', 'A MAR - BUR'): 2736,
('F BRE - ENG', 'A PAR - BUR', 'A MAR - SPA'): 1904,
('F BRE - ENG', 'A PAR - BUR', 'A MAR - PIE'): 1121},

'GERMANY': {('F KIE - DEN', 'A MUN - RUH', 'A BER - KIE'): 8054,
('F KIE - HOL', 'A MUN - RUH', 'A BER - KIE'): 3607,
('F KIE - DEN', 'A MUN - BUR', 'A BER - KIE'): 3295,
('F KIE - HOL', 'A MUN - BUR', 'A BER - KIE'): 1229,
('F KIE - DEN', 'A MUN - RUH', 'A BER - SIL'): 284,
('F KIE - HOL', 'A MUN - TYR', 'A BER - SIL'): 2095,
('F KIE - DEN', 'A MUN H', 'A BER - KIE'): 439,
('F KIE - HOL', 'A MUN H', 'A BER - KIE'): 326,
('F KIE - HOL', 'A MUN - BUR', 'A BER - SIL'): 315,
('F KIE - DEN', 'A MUN - TYR', 'A BER - KIE'): 311},

'ITALY': {('F NAP - ION', 'A ROM - VEN', 'A VEN - TYR'): 5459,
('F NAP - ION', 'A ROM - APU', 'A VEN H'): 3048,
('F NAP - ION', 'A ROM - VEN', 'A VEN - PIE'): 2095,
('F NAP - ION', 'A ROM - NAP', 'A VEN H'): 775,
('F NAP - TYS', 'A ROM - VEN', 'A VEN - PIE'): 592,
('F NAP - ION', 'A ROM - APU', 'A VEN - TRI'): 865,
('F NAP - ION', 'A ROM - VEN', 'A VEN - TRI'): 1655,
('F NAP - ION', 'A ROM - APU', 'A VEN - TYR'): 1712,
('F NAP - ION', 'A ROM - APU', 'A VEN S F TRI'): 1924,
('F NAP - ION', 'A ROM - APU', 'A VEN - PIE'): 826},

'RUSSIA': {('F STP/SC - BOT', 'A MOS - UKR', 'A WAR - GAL', 'F SEV - BLA'): 9926,
('F STP/SC - BOT', 'A MOS - UKR', 'A WAR - GAL', 'F SEV - RUM'): 970,
('F STP/SC - BOT', 'A MOS - STP', 'A WAR - UKR', 'F SEV - BLA'): 1231,
('F STP/SC - BOT', 'A MOS - UKR', 'A WAR H', 'F SEV - BLA'): 498,
('F STP/SC - BOT', 'A MOS - STP', 'A WAR - UKR', 'F SEV - RUM'): 280,
('F STP/SC - BOT', 'A MOS - STP', 'A WAR - GAL', 'F SEV - BLA'): 1260,
('F STP/SC - BOT', 'A MOS - UKR', 'A WAR H', 'F SEV - RUM'): 414,
('F STP/SC - BOT', 'A MOS - STP', 'A WAR - GAL', 'F SEV - RUM'): 302,
('F STP/SC - FIN', 'A MOS - UKR', 'A WAR - GAL', 'F SEV - BLA', ): 934,
('F STP/SC - BOT', 'A MOS - SEV', 'A WAR - GAL', 'F SEV - RUM'): 363},

'TURKEY': {('F ANK - BLA', 'A SMY - CON', 'A CON - BUL'): 12382,
('F ANK - BLA', 'A SMY - ARM', 'A CON - BUL'): 4927,
('F ANK - CON', 'A SMY H', 'A CON - BUL'): 450,
('F ANK - CON', 'A SMY - ANK', 'A CON - BUL'): 1144,
('F ANK H', 'A SMY - CON', 'A CON - BUL'): 258,
('F ANK - CON', 'A SMY - ARM', 'A CON - BUL'): 477,
('F ANK - BLA', 'A SMY - ANK', 'A CON - BUL'): 165,
('F ANK - BLA', 'A SMY H', 'A CON - BUL'): 138,
('F ANK - ARM', 'A SMY - CON', 'A CON - BUL'): 133,
('F ANK S F SEV - BLA', 'A SMY - CON', 'A CON - BUL'): 91}}

# Sampling for distribution
if power_name not in gunboat_openings:
return []
orders, counts = list(gunboat_openings[power_name]), list(gunboat_openings[power_name].values())
probs = [float(count) / sum(counts) for count in counts]
order_ix = choice(range(len(orders)), size=1, p=probs)[0]
return list(orders[order_ix])

0 comments on commit 6f6b4bd

Please sign in to comment.