diff --git a/pelita/game.py b/pelita/game.py index 2bf21dcac..89d62adee 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -39,6 +39,9 @@ #: Food pellet shadow distance SHADOW_DISTANCE = 1 +#: Proportion of layouts with dead ends +DEAD_ENDS = 0.25 + class TkViewer: def __init__(self, *, address, controller, geometry=None, delay=None, stop_after=None, fullscreen=False): self.proc = self._run_external_viewer(address, controller, geometry=geometry, delay=delay, stop_after=stop_after, fullscreen=fullscreen) diff --git a/pelita/layout.py b/pelita/layout.py index 6a880baa9..6307806de 100644 --- a/pelita/layout.py +++ b/pelita/layout.py @@ -1,5 +1,6 @@ import importlib.resources as importlib_resources import io +import os import random # bot to index conversion @@ -9,7 +10,7 @@ RNG = random.Random() -def get_random_layout(size='normal', seed=None, dead_ends=False): +def get_random_layout(size='normal', seed=None, dead_ends=0): """ Return a random layout string from the available ones. Parameters @@ -23,9 +24,9 @@ def get_random_layout(size='normal', seed=None, dead_ends=False): 'big' -> width=64, height=32, food=60 'all' -> all of the above - dead_ends: bool - if set, return a layout from the collection with dead_ends, otherwise - return a layout without dead_ends + dead_ends: float + Return a layout from the collection with dead ends with probabilty dead_ends. + By default never return a layout with dead_ends. Returns ------- @@ -35,7 +36,10 @@ def get_random_layout(size='normal', seed=None, dead_ends=False): """ if seed is not None: RNG.seed(seed) - layouts_names = get_available_layouts(size=size, dead_ends=dead_ends) + if dead_ends and RNG.random() < dead_ends: + layouts_names = get_available_layouts(size=size, dead_ends=True) + else: + layouts_names = get_available_layouts(size=size, dead_ends=False) layout_choice = RNG.choice(layouts_names) return layout_choice, get_layout_by_name(layout_choice) @@ -71,13 +75,13 @@ def get_available_layouts(size='normal', dead_ends=False): size = '' av_layouts = [] - for resource in importlib_resources.files('pelita._layouts').iterdir(): - if resource.is_file() and resource.name.endswith('.layout') and size in resource.name: - layout_name = resource.name.removesuffix('.layout') - if dead_ends and 'dead_ends' in resource.name: - av_layouts.append(layout_name) - if not dead_ends and 'dead_ends' not in resource.name: - av_layouts.append(layout_name) + for file in os.listdir(importlib_resources.files('pelita._layouts')): + if dead_ends: + cond = file.endswith('.layout') and size in file and 'dead_ends' in file + else: + cond = file.endswith('.layout') and size in file and 'dead_ends' not in file + if cond: + av_layouts.append(file.removesuffix('.layout')) return sorted(av_layouts) diff --git a/pelita/scripts/pelita_main.py b/pelita/scripts/pelita_main.py index a9251f826..821f2f392 100755 --- a/pelita/scripts/pelita_main.py +++ b/pelita/scripts/pelita_main.py @@ -14,6 +14,7 @@ import zmq import pelita +from pelita.game import DEAD_ENDS from .script_utils import start_logging from pelita.network import PELITA_PORT @@ -324,7 +325,8 @@ def main(): sys.exit(0) if args.list_layouts: - layouts = pelita.layout.get_available_layouts(size='all') + layouts = pelita.layout.get_available_layouts(size='all', dead_ends=False) + layouts += pelita.layout.get_available_layouts(size='all', dead_ends=True) layouts.sort() print('\n'.join(layouts)) sys.exit(0) @@ -447,7 +449,7 @@ def main(): layout_name = args.layout layout_string = pelita.layout.get_layout_by_name(args.layout) else: - layout_name, layout_string = pelita.layout.get_random_layout(args.size, seed=seed) + layout_name, layout_string = pelita.layout.get_random_layout(args.size, seed=seed, dead_ends=DEAD_ENDS) print("Using layout '%s'" % layout_name) diff --git a/pelita/utils.py b/pelita/utils.py index d005dab18..0167bf8ab 100644 --- a/pelita/utils.py +++ b/pelita/utils.py @@ -4,7 +4,7 @@ from .team import make_bots, create_homezones -from .game import split_food, SHADOW_DISTANCE +from .game import split_food, SHADOW_DISTANCE, DEAD_ENDS from .layout import (get_random_layout, get_layout_by_name, get_available_layouts, parse_layout, BOT_N2I, initial_positions) from .gamestate_filters import manhattan_dist @@ -20,7 +20,7 @@ def _parse_layout_arg(*, layout=None, food=None, bots=None, seed=None): # prepare layout argument to be passed to pelita.game.run_game if layout is None: - layout_name, layout_str = get_random_layout(size='normal', seed=seed) + layout_name, layout_str = get_random_layout(size='normal', seed=seed, dead_ends=DEAD_ENDS) layout_dict = parse_layout(layout_str) elif layout in get_available_layouts(size='all'): # check if this is a built-in layout diff --git a/test/test_layout.py b/test/test_layout.py index 7685d68d2..d17cdd6aa 100644 --- a/test/test_layout.py +++ b/test/test_layout.py @@ -1,6 +1,8 @@ import pytest import itertools +import math +import random from pathlib import Path from textwrap import dedent @@ -69,6 +71,21 @@ def test_get_random_layout_random_seed(): name, layout = get_random_layout(size='small', seed=1) assert name == 'small_017' +def test_get_random_layout_proportion_dead_ends(): + N = 1000 + prop = 0.25 + expected = int(prop*N) + # get a fix sequence of seeds, so that the test is reproducible + RNG = random.Random() + RNG.seed(176399) + seeds = [RNG.random() for i in range(N)] + # check that we don't get any layout with dead ends if we don't ask for it + assert not any('dead_ends' in get_random_layout(seed=s)[0] for s in seeds) + # check that we get more or less the right proportion of layouts with dead ends + dead_ends = sum('dead_ends' in get_random_layout(seed=s, dead_ends=prop)[0] for s in seeds) + assert math.isclose(dead_ends, expected, rel_tol=0.1) + + def test_legal_layout(): layout = """ ######