From 9b0f8a509d14f59c441914b79fbf5c5deffb364b Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Wed, 31 Jul 2024 16:14:40 +0200 Subject: [PATCH 01/28] WIP: Food lifetimes --- pelita/game.py | 56 +++++++++++++++++++------ pelita/gamestate_filters.py | 76 +++++++++++++++++++++++++++++++--- pelita/scripts/pelita_main.py | 2 + pelita/ui/tk_canvas.py | 10 +++-- pelita/ui/tk_sprites.py | 17 ++++++++ test/test_filter_gamestates.py | 25 ++++++++++- 6 files changed, 165 insertions(+), 21 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index 1e42eb79a..a6458bbbb 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -10,7 +10,7 @@ from . import layout from .exceptions import FatalException, NonFatalException, NoFoodWarning, PlayerTimeout -from .gamestate_filters import noiser +from .gamestate_filters import noiser, update_food_lifetimes, relocate_expired_food from .layout import initial_positions, get_legal_positions from .network import setup_controller, ZMQPublisher from .team import make_team @@ -32,6 +32,9 @@ #: The radius for the uniform noise NOISE_RADIUS = 5 +#: The lifetime of food pellets in a shadow in turns +MAX_FOOD_LIFETIME = 15 * 4 + class TkViewer: def __init__(self, *, address, controller, geometry=None, delay=None, stop_after=None): @@ -76,9 +79,10 @@ def controller_exit(state, await_action='play_step'): elif todo in ('play_step', 'set_initial'): return False -def run_game(team_specs, *, layout_dict, layout_name="", max_rounds=300, seed=None, - error_limit=5, timeout_length=3, viewers=None, viewer_options=None, - store_output=False, team_names=(None, None), team_infos=(None, None), +def run_game(team_specs, *, layout_dict, layout_name="", max_rounds=300, + seed=None, allow_squatting=False, error_limit=5, timeout_length=3, + viewers=None, viewer_options=None, store_output=False, + team_names=(None, None), team_infos=(None, None), allow_exceptions=False, print_result=True): """ Run a pelita match. @@ -180,10 +184,14 @@ def run_game(team_specs, *, layout_dict, layout_name="", max_rounds=300, seed=No # in background games # we create the initial game state - state = setup_game(team_specs, layout_dict=layout_dict, layout_name=layout_name, max_rounds=max_rounds, - error_limit=error_limit, timeout_length=timeout_length, seed=seed, - viewers=viewers, viewer_options=viewer_options, - store_output=store_output, team_names=team_names, team_infos=team_infos, + state = setup_game(team_specs, layout_dict=layout_dict, + layout_name=layout_name, max_rounds=max_rounds, + allow_squatting=allow_squatting, + error_limit=error_limit, timeout_length=timeout_length, + seed=seed, viewers=viewers, + viewer_options=viewer_options, + store_output=store_output, team_names=team_names, + team_infos=team_infos, print_result=print_result) # Play the game until it is gameover. @@ -254,8 +262,9 @@ def setup_viewers(viewers=None, options=None, print_result=True): def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed=None, - error_limit=5, timeout_length=3, viewers=None, viewer_options=None, - store_output=False, team_names=(None, None), team_infos=(None, None), + allow_squatting=False, error_limit=5, timeout_length=3, + viewers=None, viewer_options=None, store_output=False, + team_names=(None, None), team_infos=(None, None), allow_exceptions=False, print_result=True): """ Generates a game state for the given teams and layout with otherwise default values. """ @@ -292,6 +301,10 @@ def split_food(width, food): return team_food food = split_food(width, layout_dict['food']) + food_lifetime = {} + for f_team in food: + for food_item in f_team: + food_lifetime[food_item] = MAX_FOOD_LIFETIME # warn if one of the food lists is already empty side_no_food = [idx for idx, f in enumerate(food) if len(f) == 0] @@ -313,6 +326,9 @@ def split_food(width, food): #: Food per team. List of sets of (int, int) food=food, + #: Food lifetimes + food_lifetime=food_lifetime, ## allow_squatting=False, + ### Round/turn information #: Current bot, int, None turn=None, @@ -641,6 +657,9 @@ def prepare_viewer_state(game_state): del viewer_state['rnd'] del viewer_state['viewers'] del viewer_state['controller'] + + # We must transform the food lifetime dict to a list or we cannot serialise it + viewer_state['food_lifetime'] = list(viewer_state['food_lifetime'].items()) return viewer_state @@ -661,12 +680,16 @@ def play_turn(game_state, allow_exceptions=False): if game_state['gameover']: raise ValueError("Game is already over!") + game_state.update(update_food_lifetimes(game_state, NOISE_RADIUS)) + game_state.update(relocate_expired_food(game_state)) + # Now update the round counter game_state.update(next_round_turn(game_state)) turn = game_state['turn'] round = game_state['round'] team = turn % 2 + # request a new move from the current team try: position_dict = request_new_position(game_state) @@ -842,7 +865,7 @@ def apply_move(gamestate, bot_position): if bot_in_homezone: killed_enemies = [idx for idx in enemy_idx if bot_position == bots[idx]] for enemy_idx in killed_enemies: - _logger.info(f"Bot {turn} eats enemy bot {enemy_idx} at {bot_position}.") + _logger.info(f"Bot {turn} eats enemy bot {enemy_idx} at {bot_position}.") score[team] = score[team] + KILL_POINTS init_positions = initial_positions(walls, shape) bots[enemy_idx] = init_positions[enemy_idx] @@ -854,7 +877,7 @@ def apply_move(gamestate, bot_position): # check if we have been eaten enemies_on_target = [idx for idx in enemy_idx if bots[idx] == bot_position] if len(enemies_on_target) > 0: - _logger.info(f"Bot {turn} was eaten by bots {enemies_on_target} at {bot_position}.") + _logger.info(f"Bot {turn} was eaten by bots {enemies_on_target} at {bot_position}.") score[1 - team] = score[1 - team] + KILL_POINTS init_positions = initial_positions(walls, shape) bots[turn] = init_positions[turn] @@ -999,6 +1022,15 @@ def check_exit_remote_teams(game_state): pass +def split_food(shape, food): + width = shape[0] + team_food = [set(), set()] + for pos in food: + idx = pos[0] // (width // 2) + team_food[idx].add(pos) + return team_food + + def game_print(turn, msg): allow_unicode = not _mswindows if turn % 2 == 0: diff --git a/pelita/gamestate_filters.py b/pelita/gamestate_filters.py index 2f0b7354f..39b360057 100644 --- a/pelita/gamestate_filters.py +++ b/pelita/gamestate_filters.py @@ -1,8 +1,6 @@ """ collecting the game state filter functions """ import random -### The main function - def noiser(walls, shape, bot_position, enemy_positions, noise_radius=5, sight_distance=5, rnd=None): """Function to make bot positions noisy in a game state. @@ -76,9 +74,6 @@ def noiser(walls, shape, bot_position, enemy_positions, noise_radius=5, sight_di return { "enemy_positions": noised_positions, "is_noisy": is_noisy } -### The subfunctions - - def alter_pos(bot_pos, noise_radius, rnd, walls, shape): """ alter the position """ @@ -121,6 +116,77 @@ def alter_pos(bot_pos, noise_radius, rnd, walls, shape): # return the final_pos and a flag if it is noisy or not return (final_pos, noisy) +def in_homezone(position, team_id, shape): + boundary = shape[0] / 2 + if team_id == 0: + return position[0] < boundary + elif team_id == 1: + return position[0] >= boundary + + +def update_food_lifetimes(game_state, radius): + shape = game_state['shape'] + bots = game_state['bots'] + team_food = game_state['food'] + food_lifetime = dict(game_state['food_lifetime']) + + for team_idx in [0, 1]: + team_bot_pos = bots[team_idx::2] + for food_pos in team_food[team_idx]: + if any(manhattan_dist(food_pos, bot_pos) < radius and in_homezone(bot_pos, team_idx, shape) + for bot_pos in team_bot_pos): + food_lifetime[food_pos] -= 1 + else: + food_lifetime[food_pos] = 60 + + return {'food_lifetime': food_lifetime} + +def find_free_pos(shape, walls, food_to_keep, team_id): + # Finds a position in the homezone on team_id that has no walls and no food + if team_id == 0: + range_x = [0, shape[0] // 2] + else: + range_x = [shape[0] // 2, shape[0]] + range_y = [0, shape[1]] + while True: + x = random.randrange(*range_x) + y = random.randrange(*range_y) + pos = (x, y) + if pos not in walls and pos not in food_to_keep: + return pos + + +def relocate_expired_food(game_state): + team_food = game_state['food'] + food_lifetime = dict(game_state['food_lifetime']) + shape = game_state['shape'] + walls = game_state['walls'] + + res_food = [] + + for team_idx in [0, 1]: + food_to_relocate = set() + food_to_keep = set() + for food_pos in team_food[team_idx]: + if food_lifetime[food_pos] == 0: + food_to_relocate.add(food_pos) + del food_lifetime[food_pos] + else: + food_to_keep.add(food_pos) + + for relocate in food_to_relocate: + new_pos = find_free_pos(shape, walls, food_to_keep, team_idx) + food_to_keep.add(new_pos) + food_lifetime[new_pos] = 60 + + res_food.append(food_to_keep) + + res = { + "food": res_food, + "food_lifetime": food_lifetime + } + + return res def manhattan_dist(pos1, pos2): """ Manhattan distance between two points. diff --git a/pelita/scripts/pelita_main.py b/pelita/scripts/pelita_main.py index 30aef0598..71a44037a 100755 --- a/pelita/scripts/pelita_main.py +++ b/pelita/scripts/pelita_main.py @@ -221,6 +221,8 @@ def long_help(s): help='Maximum number of rounds to play.') game_settings.add_argument('--seed', type=int, metavar='SEED', default=None, help='Initialize the random number generator with SEED.') +game_settings.add_argument('--allow-squatting', type=bool, default=False, + help='do not set a food lifetime') layout_opt = game_settings.add_mutually_exclusive_group() layout_opt.add_argument('--layout', metavar='LAYOUT', diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index a2befdacd..ffbd8424a 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -413,12 +413,14 @@ def update(self, game_state=None): eaten_food = [] for food_pos, food_item in self.food_items.items(): + food_item.food_lifetime = game_state['food_lifetime'][food_pos] if not food_pos in game_state["food"]: self.ui.game_canvas.delete(food_item.tag) eaten_food.append(food_pos) for food_pos in eaten_food: del self.food_items[food_pos] + winning_team_idx = game_state.get("whowins") if winning_team_idx is None: self.draw_end_of_game(None) @@ -788,13 +790,14 @@ def clear(self): self.ui.game_canvas.delete(tkinter.ALL) def draw_food(self, game_state): - if not self.size_changed: - return +# if not self.size_changed: +# return self.ui.game_canvas.delete("food") self.food_items = {} for position in game_state['food']: model_x, model_y = position - food_item = Food(self.mesh_graph, position=(model_x, model_y)) + lifetime = game_state['food_lifetime'][position] + food_item = Food(self.mesh_graph, position=(model_x, model_y), food_lifetime=lifetime) food_item.draw(self.ui.game_canvas) self.food_items[position] = food_item @@ -966,6 +969,7 @@ def observe(self, game_state): game_state['food'] = _ensure_list_tuples(game_state['food']) game_state['bots'] = _ensure_list_tuples(game_state['bots']) game_state['shape'] = tuple(game_state['shape']) + game_state['food_lifetime'] = {tuple(pos): lifetime for pos, lifetime in game_state['food_lifetime']} self.update(game_state) if self._stop_after is not None: if self._stop_after == 0: diff --git a/pelita/ui/tk_sprites.py b/pelita/ui/tk_sprites.py index 59b9376ac..5baac4231 100644 --- a/pelita/ui/tk_sprites.py +++ b/pelita/ui/tk_sprites.py @@ -294,6 +294,10 @@ def draw(self, canvas, game_state=None): class Food(TkSprite): + def __init__(self, mesh, food_lifetime=None, **kwargs): + self.food_lifetime = food_lifetime + super().__init__(mesh, **kwargs) + @classmethod def food_pos_tag(cls, position): return "Food" + str(position) @@ -305,6 +309,19 @@ def draw(self, canvas, game_state=None): fill = RED canvas.create_oval(self.bounding_box(0.4), fill=fill, width=0, tag=(self.tag, self.food_pos_tag(self.position), "food")) + canvas.delete("show_lifetime" + str(self.position)) + lifetime = self.food_lifetime + # we print the bot_id in the lower left corner + if self.food_lifetime: + shift_x = 32 + shift_y = 16 + tag=(self.tag, "show_lifetime" + str(self.position), "food") + canvas.create_text(self.bounding_box()[0][0]-1 + shift_x, self.bounding_box()[1][1] - shift_y, text=lifetime, font=(None, 12), fill="white", tag=tag) + canvas.create_text(self.bounding_box()[0][0]+1 + shift_x, self.bounding_box()[1][1] - shift_y, text=lifetime, font=(None, 12), fill="white", tag=tag) + canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1]-1 - shift_y, text=lifetime, font=(None, 12), fill="white", tag=tag) + canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1]+1 - shift_y, text=lifetime, font=(None, 12), fill="white", tag=tag) + canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1] - shift_y, text=lifetime, font=(None, 12), fill="black", tag=tag) + class Arrow(TkSprite): def __init__(self, mesh, req_pos, success, **kwargs): diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index b1562f703..c44ec5a60 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -5,7 +5,7 @@ import random from pelita import gamestate_filters as gf -from pelita.game import setup_game, prepare_bot_state +from pelita.game import setup_game, prepare_bot_state, split_food from pelita.layout import parse_layout @@ -587,3 +587,26 @@ def test_noise_manhattan_failure(): assert noised['is_noisy'] == [False, False] noised_pos = noised['enemy_positions'] assert noised_pos == parsed['bots'][0::2] + +def test_update_food_lifetimes(): + test_layout = ( + """ ################## + # #. . # . b # + # ##### #####y# + # a . # . .#x# + ################## """) + parsed = parse_layout(test_layout) + food_lifetime = {pos: 60 for pos in parsed['food']} + food = split_food(parsed['shape'], parsed['food']) + + parsed.update({ + "food": food, + "food_lifetime": food_lifetime, + }) + + assert gf.update_food_lifetimes(parsed, 2)['food_lifetime'] == {(3, 1): 60, (6, 1): 60, (6, 3): 60, (11, 1): 60, (11, 3): 60, (14, 3): 60} + assert gf.update_food_lifetimes(parsed, 3)['food_lifetime'] == {(3, 1): 59, (6, 1): 60, (6, 3): 60, (11, 1): 60, (11, 3): 60, (14, 3): 59} + assert gf.update_food_lifetimes(parsed, 5)['food_lifetime'] == {(3, 1): 59, (6, 1): 60, (6, 3): 59, (11, 1): 60, (11, 3): 60, (14, 3): 59} + +def test_relocate_expired_food(): + pass \ No newline at end of file From 3b93130999539ca99aaf00fbb6d5baac07ae198c Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 16:09:36 +0200 Subject: [PATCH 02/28] rename allow_squatting to allow_camping, which is the standard term in the field --- pelita/game.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index a6458bbbb..2562a35a7 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -80,7 +80,7 @@ def controller_exit(state, await_action='play_step'): return False def run_game(team_specs, *, layout_dict, layout_name="", max_rounds=300, - seed=None, allow_squatting=False, error_limit=5, timeout_length=3, + seed=None, allow_camping=False, error_limit=5, timeout_length=3, viewers=None, viewer_options=None, store_output=False, team_names=(None, None), team_infos=(None, None), allow_exceptions=False, print_result=True): @@ -186,7 +186,7 @@ def run_game(team_specs, *, layout_dict, layout_name="", max_rounds=300, # we create the initial game state state = setup_game(team_specs, layout_dict=layout_dict, layout_name=layout_name, max_rounds=max_rounds, - allow_squatting=allow_squatting, + allow_camping=allow_camping, error_limit=error_limit, timeout_length=timeout_length, seed=seed, viewers=viewers, viewer_options=viewer_options, @@ -262,7 +262,7 @@ def setup_viewers(viewers=None, options=None, print_result=True): def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed=None, - allow_squatting=False, error_limit=5, timeout_length=3, + allow_camping=False, error_limit=5, timeout_length=3, viewers=None, viewer_options=None, store_output=False, team_names=(None, None), team_infos=(None, None), allow_exceptions=False, print_result=True): @@ -327,7 +327,7 @@ def split_food(width, food): food=food, #: Food lifetimes - food_lifetime=food_lifetime, ## allow_squatting=False, + food_lifetime=food_lifetime, ## allow_camping=False, ### Round/turn information #: Current bot, int, None From cfa58846385381a58726cc1065c1eaf60f8ca9cc Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 16:37:33 +0200 Subject: [PATCH 03/28] get rid of double/triple defined split_food --- pelita/game.py | 10 +--------- pelita/utils.py | 8 +------- test/test_filter_gamestates.py | 4 ++-- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index 2562a35a7..d7e7a4979 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -293,13 +293,6 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= if not (0, 0) <= pos < (width, height): raise ValueError(f"Bot {idx} is not inside the layout: {pos}.") - def split_food(width, food): - team_food = [set(), set()] - for pos in food: - idx = pos[0] // (width // 2) - team_food[idx].add(pos) - return team_food - food = split_food(width, layout_dict['food']) food_lifetime = {} for f_team in food: @@ -1022,8 +1015,7 @@ def check_exit_remote_teams(game_state): pass -def split_food(shape, food): - width = shape[0] +def split_food(width, food): team_food = [set(), set()] for pos in food: idx = pos[0] // (width // 2) diff --git a/pelita/utils.py b/pelita/utils.py index 10f154732..ef44921b1 100644 --- a/pelita/utils.py +++ b/pelita/utils.py @@ -4,6 +4,7 @@ from .team import make_bots, create_homezones +from .game import split_food from .layout import (get_random_layout, get_layout_by_name, get_available_layouts, parse_layout, BOT_N2I, initial_positions) @@ -212,13 +213,6 @@ def setup_test_game(*, layout, is_blue=True, round=None, score=None, seed=None, width, height = layout['shape'] - def split_food(width, food): - team_food = [set(), set()] - for pos in food: - idx = pos[0] // (width // 2) - team_food[idx].add(pos) - return team_food - food = split_food(width, layout['food']) if is_blue: diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index c44ec5a60..e587a1f9e 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -597,7 +597,7 @@ def test_update_food_lifetimes(): ################## """) parsed = parse_layout(test_layout) food_lifetime = {pos: 60 for pos in parsed['food']} - food = split_food(parsed['shape'], parsed['food']) + food = split_food(parsed['shape'][0], parsed['food']) parsed.update({ "food": food, @@ -609,4 +609,4 @@ def test_update_food_lifetimes(): assert gf.update_food_lifetimes(parsed, 5)['food_lifetime'] == {(3, 1): 59, (6, 1): 60, (6, 3): 59, (11, 1): 60, (11, 3): 60, (14, 3): 59} def test_relocate_expired_food(): - pass \ No newline at end of file + pass From 764dcff9734938ce3075f9dbab259502636a8074 Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 17:27:37 +0200 Subject: [PATCH 04/28] support enabling camping by setting max_food_lifetime to infinity --- pelita/game.py | 9 +++++++-- pelita/gamestate_filters.py | 6 ++++-- test/test_filter_gamestates.py | 6 +++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index d7e7a4979..2c7bc396e 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -6,6 +6,7 @@ import subprocess import sys import time +import math from warnings import warn from . import layout @@ -295,9 +296,10 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= food = split_food(width, layout_dict['food']) food_lifetime = {} + max_food_lifetime = math.inf if allow_camping else MAX_FOOD_LIFETIME for f_team in food: for food_item in f_team: - food_lifetime[food_item] = MAX_FOOD_LIFETIME + food_lifetime[food_item] = max_food_lifetime # warn if one of the food lists is already empty side_no_food = [idx for idx, f in enumerate(food) if len(f) == 0] @@ -320,7 +322,10 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= food=food, #: Food lifetimes - food_lifetime=food_lifetime, ## allow_camping=False, + food_lifetime=food_lifetime, + + #: Max food lifetime + max_food_lifetime=max_food_lifetime, ### Round/turn information #: Current bot, int, None diff --git a/pelita/gamestate_filters.py b/pelita/gamestate_filters.py index 39b360057..1bb810728 100644 --- a/pelita/gamestate_filters.py +++ b/pelita/gamestate_filters.py @@ -124,11 +124,13 @@ def in_homezone(position, team_id, shape): return position[0] >= boundary -def update_food_lifetimes(game_state, radius): +def update_food_lifetimes(game_state, radius, max_food_lifetime=None): shape = game_state['shape'] bots = game_state['bots'] team_food = game_state['food'] food_lifetime = dict(game_state['food_lifetime']) + if max_food_lifetime is None: + max_food_lifetime = game_state['max_food_lifetime'] for team_idx in [0, 1]: team_bot_pos = bots[team_idx::2] @@ -137,7 +139,7 @@ def update_food_lifetimes(game_state, radius): for bot_pos in team_bot_pos): food_lifetime[food_pos] -= 1 else: - food_lifetime[food_pos] = 60 + food_lifetime[food_pos] = max_food_lifetime return {'food_lifetime': food_lifetime} diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index e587a1f9e..7961eda1d 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -604,9 +604,9 @@ def test_update_food_lifetimes(): "food_lifetime": food_lifetime, }) - assert gf.update_food_lifetimes(parsed, 2)['food_lifetime'] == {(3, 1): 60, (6, 1): 60, (6, 3): 60, (11, 1): 60, (11, 3): 60, (14, 3): 60} - assert gf.update_food_lifetimes(parsed, 3)['food_lifetime'] == {(3, 1): 59, (6, 1): 60, (6, 3): 60, (11, 1): 60, (11, 3): 60, (14, 3): 59} - assert gf.update_food_lifetimes(parsed, 5)['food_lifetime'] == {(3, 1): 59, (6, 1): 60, (6, 3): 59, (11, 1): 60, (11, 3): 60, (14, 3): 59} + assert gf.update_food_lifetimes(parsed, 2, 60)['food_lifetime'] == {(3, 1): 60, (6, 1): 60, (6, 3): 60, (11, 1): 60, (11, 3): 60, (14, 3): 60} + assert gf.update_food_lifetimes(parsed, 3, 60)['food_lifetime'] == {(3, 1): 59, (6, 1): 60, (6, 3): 60, (11, 1): 60, (11, 3): 60, (14, 3): 59} + assert gf.update_food_lifetimes(parsed, 5, 60)['food_lifetime'] == {(3, 1): 59, (6, 1): 60, (6, 3): 59, (11, 1): 60, (11, 3): 60, (14, 3): 59} def test_relocate_expired_food(): pass From 941bb4e1dfd1a88d9f77682ee0fde39907646747 Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 17:29:04 +0200 Subject: [PATCH 05/28] fix test that would occasionally fail if camping is not allowed... ... given the minimal layout, food is relocated all the time and because the test logic assumes that some food pellets are not reacheable, the test doesn't work anymore reliably. --- test/test_team.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_team.py b/test/test_team.py index 6544ed7cc..df0ff3940 100644 --- a/test/test_team.py +++ b/test/test_team.py @@ -118,7 +118,7 @@ def trackingBot(bot, state): trackingBot ] # We play 600 rounds as we rely on some randomness in our assertions - state = setup_game(team, max_rounds=600, layout_dict=parse_layout(layout)) + state = setup_game(team, max_rounds=600, allow_camping=True, layout_dict=parse_layout(layout)) while not state['gameover']: # Check that our count is consistent with what the game thinks # for the current and previous bot, we have to subtract the deaths that have just respawned @@ -454,7 +454,7 @@ def test_bot_graph_is_half_mutable(): """ observer = [] - + def blue(bot, state): if bot.turn == 0 and bot.round == 1: assert bot.graph[1, 1][1, 2].get('weight') is None From 1599390b09fd81631c5f02c1382ecd2dbed775c1 Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 18:01:27 +0200 Subject: [PATCH 06/28] for each team update food lifetimes only once per round --- pelita/game.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index 2c7bc396e..e1f9999ed 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -33,8 +33,8 @@ #: The radius for the uniform noise NOISE_RADIUS = 5 -#: The lifetime of food pellets in a shadow in turns -MAX_FOOD_LIFETIME = 15 * 4 +#: The lifetime of food pellets in a shadow in rounds +MAX_FOOD_LIFETIME = 15 class TkViewer: @@ -678,9 +678,6 @@ def play_turn(game_state, allow_exceptions=False): if game_state['gameover']: raise ValueError("Game is already over!") - game_state.update(update_food_lifetimes(game_state, NOISE_RADIUS)) - game_state.update(relocate_expired_food(game_state)) - # Now update the round counter game_state.update(next_round_turn(game_state)) @@ -688,6 +685,13 @@ def play_turn(game_state, allow_exceptions=False): round = game_state['round'] team = turn % 2 + if turn >= 2: + # update food_lifetimes only one time per round per team + # otherwise pellets that are in the shadow of two bots + # would get the lifetime reduced by 2 within a round + game_state.update(update_food_lifetimes(game_state, NOISE_RADIUS)) + game_state.update(relocate_expired_food(game_state)) + # request a new move from the current team try: position_dict = request_new_position(game_state) From 7fb2d6bba5dab1ecacde26877a1956e96d44300c Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 19:25:28 +0200 Subject: [PATCH 07/28] introduce a dedicated parameter for a bot shadow for food expiration and refactor lifetime updating --- pelita/game.py | 4 +++- pelita/gamestate_filters.py | 21 +++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index e1f9999ed..9b8a6fab5 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -36,6 +36,8 @@ #: The lifetime of food pellets in a shadow in rounds MAX_FOOD_LIFETIME = 15 +#: Food pellet lifetime distance +LIFETIME_DISTANCE = 3 class TkViewer: def __init__(self, *, address, controller, geometry=None, delay=None, stop_after=None): @@ -689,7 +691,7 @@ def play_turn(game_state, allow_exceptions=False): # update food_lifetimes only one time per round per team # otherwise pellets that are in the shadow of two bots # would get the lifetime reduced by 2 within a round - game_state.update(update_food_lifetimes(game_state, NOISE_RADIUS)) + game_state.update(update_food_lifetimes(game_state, team, LIFETIME_DISTANCE)) game_state.update(relocate_expired_food(game_state)) # request a new move from the current team diff --git a/pelita/gamestate_filters.py b/pelita/gamestate_filters.py index 1bb810728..8ab38c1c8 100644 --- a/pelita/gamestate_filters.py +++ b/pelita/gamestate_filters.py @@ -124,22 +124,19 @@ def in_homezone(position, team_id, shape): return position[0] >= boundary -def update_food_lifetimes(game_state, radius, max_food_lifetime=None): - shape = game_state['shape'] - bots = game_state['bots'] - team_food = game_state['food'] +def update_food_lifetimes(game_state, team, radius, max_food_lifetime=None): + bots = game_state['bots'][team::2] + food = game_state['food'][team] food_lifetime = dict(game_state['food_lifetime']) if max_food_lifetime is None: max_food_lifetime = game_state['max_food_lifetime'] - for team_idx in [0, 1]: - team_bot_pos = bots[team_idx::2] - for food_pos in team_food[team_idx]: - if any(manhattan_dist(food_pos, bot_pos) < radius and in_homezone(bot_pos, team_idx, shape) - for bot_pos in team_bot_pos): - food_lifetime[food_pos] -= 1 - else: - food_lifetime[food_pos] = max_food_lifetime + for pellet in food: + if (manhattan_dist(bots[0], pellet) <= radius or + manhattan_dist(bots[1], pellet) <= radius ): + food_lifetime[pellet] -= 1 + else: + food_lifetime[pellet] = max_food_lifetime return {'food_lifetime': food_lifetime} From 1cfcacab22fa067e35f97d6a3f256b80aa8a0059 Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 19:26:05 +0200 Subject: [PATCH 08/28] refactor food relocation after expiring --- pelita/game.py | 2 +- pelita/gamestate_filters.py | 95 +++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index 9b8a6fab5..72a9bb1b8 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -692,7 +692,7 @@ def play_turn(game_state, allow_exceptions=False): # otherwise pellets that are in the shadow of two bots # would get the lifetime reduced by 2 within a round game_state.update(update_food_lifetimes(game_state, team, LIFETIME_DISTANCE)) - game_state.update(relocate_expired_food(game_state)) + game_state.update(relocate_expired_food(game_state, team, LIFETIME_DISTANCE)) # request a new move from the current team try: diff --git a/pelita/gamestate_filters.py b/pelita/gamestate_filters.py index 8ab38c1c8..a8f248c99 100644 --- a/pelita/gamestate_filters.py +++ b/pelita/gamestate_filters.py @@ -140,52 +140,63 @@ def update_food_lifetimes(game_state, team, radius, max_food_lifetime=None): return {'food_lifetime': food_lifetime} -def find_free_pos(shape, walls, food_to_keep, team_id): - # Finds a position in the homezone on team_id that has no walls and no food - if team_id == 0: - range_x = [0, shape[0] // 2] - else: - range_x = [shape[0] // 2, shape[0]] - range_y = [0, shape[1]] - while True: - x = random.randrange(*range_x) - y = random.randrange(*range_y) - pos = (x, y) - if pos not in walls and pos not in food_to_keep: - return pos - - -def relocate_expired_food(game_state): - team_food = game_state['food'] + +def relocate_expired_food(game_state, team, radius, max_food_lifetime=None): + bots = game_state['bots'][team::2] + enemy_bots = game_state['bots'][1-team::2] + food = [set(team_food) for team_food in game_state['food']] food_lifetime = dict(game_state['food_lifetime']) - shape = game_state['shape'] + width, height = game_state['shape'] walls = game_state['walls'] + rnd = game_state['rnd'] + if max_food_lifetime is None: + max_food_lifetime = game_state['max_food_lifetime'] - res_food = [] - - for team_idx in [0, 1]: - food_to_relocate = set() - food_to_keep = set() - for food_pos in team_food[team_idx]: - if food_lifetime[food_pos] == 0: - food_to_relocate.add(food_pos) - del food_lifetime[food_pos] - else: - food_to_keep.add(food_pos) - - for relocate in food_to_relocate: - new_pos = find_free_pos(shape, walls, food_to_keep, team_idx) - food_to_keep.add(new_pos) - food_lifetime[new_pos] = 60 - - res_food.append(food_to_keep) - - res = { - "food": res_food, - "food_lifetime": food_lifetime - } + # generate a set of possible positions to relocate food: + # - in the bot's homezone + # - not a wall + # - not on a already present food pellet + # - not on a bot + # - not on the border + # - not within the shadow of a bot + home_width = width // 2 + left_most_x = home_width * team + targets = { (x, y) for x in range(left_most_x, left_most_x+home_width) # this line and the next define the homezone + for y in range(height) + if (x not in (home_width, home_width - 1) # this excludes the border + and manhattan_dist(bots[0], (x, y)) > radius # this line and the next excludes the team's bots and their shadows + and manhattan_dist(bots[1], (x, y)) > radius ) + } + targets = targets.difference(walls) # remove the walls + targets = targets.difference(food[team]) # remove the team's food + targets = targets.difference(enemy_bots) # remove the enemy bots + # now convert to a list and sort, so that we have reproducibility (sets are unordered) + targets = sorted(list(targets)) + for pellet in sorted(list(food[team])): + if food_lifetime[pellet] > 0: + # the current pellet is fine, keep it! + continue + if not targets: + # we have no free positions anymore, just let the food stay where it is + # we do not update the lifetime, so this pellet will get a chance to be + # relocated at the next round + continue + # choose a new position at random + new_pos = rnd.choice(targets) + + # remove the new pellet position from the list of possible targets for new pellets + targets.remove(new_pos) + + # get rid of the old pellet + food[team].remove(pellet) + del food_lifetime[pellet] + + # track the new pellet + food_lifetime[new_pos] = max_food_lifetime + food[team].add(new_pos) + + return {'food' : food, 'food_lifetime' : food_lifetime} - return res def manhattan_dist(pos1, pos2): """ Manhattan distance between two points. From 97058f1ad5dc3eeb9d7b2d2dff6cd529e59718d3 Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 20:27:20 +0200 Subject: [PATCH 09/28] extend tests for food lifetime updating --- test/test_filter_gamestates.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index 7961eda1d..81aca53be 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -595,8 +595,9 @@ def test_update_food_lifetimes(): # ##### #####y# # a . # . .#x# ################## """) + mx = 60 parsed = parse_layout(test_layout) - food_lifetime = {pos: 60 for pos in parsed['food']} + food_lifetime = {pos: mx for pos in parsed['food']} food = split_food(parsed['shape'][0], parsed['food']) parsed.update({ @@ -604,9 +605,29 @@ def test_update_food_lifetimes(): "food_lifetime": food_lifetime, }) - assert gf.update_food_lifetimes(parsed, 2, 60)['food_lifetime'] == {(3, 1): 60, (6, 1): 60, (6, 3): 60, (11, 1): 60, (11, 3): 60, (14, 3): 60} - assert gf.update_food_lifetimes(parsed, 3, 60)['food_lifetime'] == {(3, 1): 59, (6, 1): 60, (6, 3): 60, (11, 1): 60, (11, 3): 60, (14, 3): 59} - assert gf.update_food_lifetimes(parsed, 5, 60)['food_lifetime'] == {(3, 1): 59, (6, 1): 60, (6, 3): 59, (11, 1): 60, (11, 3): 60, (14, 3): 59} + radius = 1 + expected = {(3, 1): mx, (6, 1): mx, (6, 3): mx, # team0 + (11, 1): mx, (11, 3): mx, (14, 3): mx} # team1 + # nothing should change for either team, the radius is too small + assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected + assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected + + radius = 2 + expected_team0 = {(3, 1): mx-1, (6, 1): mx, (6, 3): mx, # team0 + (11, 1): mx, (11, 3): mx, (14, 3): mx} # team1 + expected_team1 = {(3, 1): mx, (6, 1): mx, (6, 3): mx, # team0 + (11, 1): mx, (11, 3): mx, (14, 3): mx-1} # team1 + # the two teams should get exactly one pellet updated + assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected_team0 + assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected_team1 + + radius = 4 + expected_team0 = {(3, 1): mx-1, (6, 1): mx, (6, 3): mx-1, # team0 + (11, 1): mx, (11, 3): mx, (14, 3): mx} # team1 + expected_team1 = {(3, 1): mx, (6, 1): mx, (6, 3): mx, # team0 + (11, 1): mx, (11, 3): mx, (14, 3): mx-1} # team1 + assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected_team0 + assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected_team1 def test_relocate_expired_food(): pass From a73c0758b16c3cf29aa0cb898d85e937d0505f85 Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 20:27:32 +0200 Subject: [PATCH 10/28] test food relocation after expiring --- test/test_filter_gamestates.py | 53 ++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index 81aca53be..b9851a5e9 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -629,5 +629,54 @@ def test_update_food_lifetimes(): assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected_team0 assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected_team1 -def test_relocate_expired_food(): - pass +# repeat the test 20 times to exercise the randomness of the relocation algorithm +@pytest.mark.parametrize('dummy', range(20)) +def test_relocate_expired_food(dummy): + test_layout = ( + """ ################## + # #. . # . b # + # ##### #####y# + # a . # . .#x# + ################## """) + team_exp_relocate = ( (3, 1), (14, 3) ) + border = (8, 9) + for team in (0, 1): + mx = 1 + parsed = parse_layout(test_layout) + food_lifetime = {pos: mx for pos in parsed['food']} + food = split_food(parsed['shape'][0], parsed['food']) + + parsed.update({ + "food": food, + "food_lifetime": food_lifetime, + "rnd" : random.Random(), + }) + + radius = 2 + + parsed.update(gf.update_food_lifetimes(parsed, team, radius, mx)) + out = gf.relocate_expired_food(parsed, team, radius, mx) + + # check that the expired pellet is gone, bot only when it's our team turn + assert team_exp_relocate[team] not in out['food'][team] + assert team_exp_relocate[1-team] in out['food'][1-team] + + # check that the relocation was done properly + assert len(parsed['food'][team].intersection(out['food'][team])) == 2 + new = out['food'][team].difference(parsed['food'][team]) + assert len(new) == 1 # there is only one new pellet + new = new.pop() + assert new not in parsed['walls'] # it was not located on a wall + assert new[0] not in border # it was not located on the border + assert new not in parsed['bots'] # it was not located on a bot + for team_bot in parsed['bots'][team::2]: + # it was not located within the shadow of a team bot + assert gf.manhattan_dist(new, team_bot) > radius + # check that the new pellet is in the right homezone + if team == 0: + assert 0 < new[0] + assert new[0] < border[0] + else: + assert border[1] < new[0] + assert new[0] < border[0]*2 + From 7c12ecf9f8141ec3d408f9d311057c59038664d3 Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 20:48:26 +0200 Subject: [PATCH 11/28] add a test for the case when there is not space left to relocate food --- test/test_filter_gamestates.py | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index b9851a5e9..79bb56779 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -680,3 +680,43 @@ def test_relocate_expired_food(dummy): assert border[1] < new[0] assert new[0] < border[0]*2 +def test_relocate_expired_food_nospaceleft(): + test_layout = ( + """ ################## + ###..... # . b # + #######y ###### + ###a##..# . .#x# + ################## """) + to_relocate = (3, 1) + mx = 1 + parsed = parse_layout(test_layout) + food_lifetime = {pos: mx for pos in parsed['food']} + food_lifetime[to_relocate] = 0 # set to zero so that we also test that we support negative lifetimes + food = split_food(parsed['shape'][0], parsed['food']) + + parsed.update({ + "food": food, + "food_lifetime": food_lifetime, + "rnd" : random.Random(), + }) + + radius = 2 + + parsed.update(gf.update_food_lifetimes(parsed, 0, radius, mx)) + out = gf.relocate_expired_food(parsed, 0, radius, mx) + # check that the food pellet did not move: there is no space to move it + # anywhere + assert to_relocate in out['food'][0] + assert len(out['food'][0]) == 7 + assert out['food_lifetime'][to_relocate] == -1 + + # now make space for the food and check that it gets located in the free spot + parsed.update(out) + parsed['food'][0].remove((7,1)) + del parsed['food_lifetime'][(7,1)] + out = gf.relocate_expired_food(parsed, 0, radius, mx) + assert to_relocate not in out['food'][0] + assert (7, 1) in out['food'][0] + assert to_relocate not in out['food_lifetime'] + assert out['food_lifetime'][(7, 1)] == mx + From f577ba5f927b1193e01f5755b9e43d46f9cadd4f Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 22:18:05 +0200 Subject: [PATCH 12/28] update food lifetimes at every turn --- pelita/game.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index 72a9bb1b8..a940f34aa 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -33,8 +33,8 @@ #: The radius for the uniform noise NOISE_RADIUS = 5 -#: The lifetime of food pellets in a shadow in rounds -MAX_FOOD_LIFETIME = 15 +#: The lifetime of food pellets in a shadow in turns +MAX_FOOD_LIFETIME = 30 * 2 #: Food pellet lifetime distance LIFETIME_DISTANCE = 3 @@ -687,12 +687,9 @@ def play_turn(game_state, allow_exceptions=False): round = game_state['round'] team = turn % 2 - if turn >= 2: - # update food_lifetimes only one time per round per team - # otherwise pellets that are in the shadow of two bots - # would get the lifetime reduced by 2 within a round - game_state.update(update_food_lifetimes(game_state, team, LIFETIME_DISTANCE)) - game_state.update(relocate_expired_food(game_state, team, LIFETIME_DISTANCE)) + # update food lifetimes and relocate expired food for the current team + game_state.update(update_food_lifetimes(game_state, team, LIFETIME_DISTANCE)) + game_state.update(relocate_expired_food(game_state, team, LIFETIME_DISTANCE)) # request a new move from the current team try: From 409a07a64b24c59e67efb754d20238574b7619da Mon Sep 17 00:00:00 2001 From: Tiziano Zito Date: Thu, 1 Aug 2024 22:19:40 +0200 Subject: [PATCH 13/28] reset MAX_FOOD_LIFETIME so that it matches the previous value --- pelita/game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pelita/game.py b/pelita/game.py index a940f34aa..e8fa59068 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -34,7 +34,7 @@ NOISE_RADIUS = 5 #: The lifetime of food pellets in a shadow in turns -MAX_FOOD_LIFETIME = 30 * 2 +MAX_FOOD_LIFETIME = 30 #: Food pellet lifetime distance LIFETIME_DISTANCE = 3 From d651eb2ba8914a97017d96f3495f4d3e72c4e791 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Fri, 2 Aug 2024 15:54:11 +0200 Subject: [PATCH 14/28] ENH: Only ghosts can cast a shadow --- pelita/gamestate_filters.py | 9 ++++++--- test/test_filter_gamestates.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/pelita/gamestate_filters.py b/pelita/gamestate_filters.py index a8f248c99..96c980870 100644 --- a/pelita/gamestate_filters.py +++ b/pelita/gamestate_filters.py @@ -125,15 +125,18 @@ def in_homezone(position, team_id, shape): def update_food_lifetimes(game_state, team, radius, max_food_lifetime=None): - bots = game_state['bots'][team::2] + # Only ghosts can cast a shadow + ghosts = [ + bot for bot in game_state['bots'][team::2] + if in_homezone(bot, team, game_state['shape']) + ] food = game_state['food'][team] food_lifetime = dict(game_state['food_lifetime']) if max_food_lifetime is None: max_food_lifetime = game_state['max_food_lifetime'] for pellet in food: - if (manhattan_dist(bots[0], pellet) <= radius or - manhattan_dist(bots[1], pellet) <= radius ): + if any(manhattan_dist(ghost, pellet) <= radius for ghost in ghosts): food_lifetime[pellet] -= 1 else: food_lifetime[pellet] = max_food_lifetime diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index 79bb56779..de6105976 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -5,8 +5,9 @@ import random from pelita import gamestate_filters as gf -from pelita.game import setup_game, prepare_bot_state, split_food +from pelita.game import setup_game, play_turn, prepare_bot_state, split_food from pelita.layout import parse_layout +from pelita.player import stepping_player def make_gamestate(): @@ -720,3 +721,34 @@ def test_relocate_expired_food_nospaceleft(): assert to_relocate not in out['food_lifetime'] assert out['food_lifetime'][(7, 1)] == mx +def test_pacman_resets_lifetime(): + # We move bot a across the border + # Once it becomes a bot, the food_lifetime should reset + test_layout = ( + """ ################## + # .. xy# + # a # + #b # + ################## """) + team1 = stepping_player('>>-', '---') + team2 = stepping_player('---', '---') + + parsed = parse_layout(test_layout) + state = setup_game([team1, team2], layout_dict=parsed, max_rounds=8) + assert state['food_lifetime'] == {(8, 1): 30, (9, 1): 30} + state = play_turn(state) + assert state['turn'] == 0 + assert state['food_lifetime'] == {(8, 1): 29, (9, 1): 30} + state = play_turn(state) + state = play_turn(state) + state = play_turn(state) + state = play_turn(state) + # NOTE: food_lifetimes are calculated *before* the move + # Therefore this will only be updated once it is team1’s turn again + assert state['turn'] == 0 + assert state['food_lifetime'] == {(8, 1): 27, (9, 1): 30} + state = play_turn(state) + assert state['food_lifetime'] == {(8, 1): 27, (9, 1): 30} + state = play_turn(state) + assert state['turn'] == 2 + assert state['food_lifetime'] == {(8, 1): 30, (9, 1): 30} From 7d437b43db2d74283683e5c4f498faf75e3577f6 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Fri, 2 Aug 2024 16:02:34 +0200 Subject: [PATCH 15/28] TST: Test for both sides --- test/test_filter_gamestates.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index de6105976..7c7ccd796 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -726,29 +726,34 @@ def test_pacman_resets_lifetime(): # Once it becomes a bot, the food_lifetime should reset test_layout = ( """ ################## - # .. xy# - # a # - #b # + # x y# + # .. # + #b a # ################## """) team1 = stepping_player('>>-', '---') - team2 = stepping_player('---', '---') + team2 = stepping_player('<<-', '---') parsed = parse_layout(test_layout) state = setup_game([team1, team2], layout_dict=parsed, max_rounds=8) - assert state['food_lifetime'] == {(8, 1): 30, (9, 1): 30} + assert state['food_lifetime'] == {(8, 2): 30, (9, 2): 30} state = play_turn(state) assert state['turn'] == 0 - assert state['food_lifetime'] == {(8, 1): 29, (9, 1): 30} + assert state['food_lifetime'] == {(8, 2): 29, (9, 2): 30} state = play_turn(state) + assert state['turn'] == 1 + assert state['food_lifetime'] == {(8, 2): 29, (9, 2): 29} state = play_turn(state) state = play_turn(state) state = play_turn(state) # NOTE: food_lifetimes are calculated *before* the move # Therefore this will only be updated once it is team1’s turn again assert state['turn'] == 0 - assert state['food_lifetime'] == {(8, 1): 27, (9, 1): 30} + assert state['food_lifetime'] == {(8, 2): 27, (9, 2): 28} state = play_turn(state) - assert state['food_lifetime'] == {(8, 1): 27, (9, 1): 30} + assert state['food_lifetime'] == {(8, 2): 27, (9, 2): 27} state = play_turn(state) assert state['turn'] == 2 - assert state['food_lifetime'] == {(8, 1): 30, (9, 1): 30} + assert state['food_lifetime'] == {(8, 2): 30, (9, 2): 27} + state = play_turn(state) + assert state['turn'] == 3 + assert state['food_lifetime'] == {(8, 2): 30, (9, 2): 30} From 68af7647a6ea00dfc899fc016b3ae9ae122c6091 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Sun, 4 Aug 2024 23:30:39 +0200 Subject: [PATCH 16/28] =?UTF-8?q?RF:=20Only=20include=20food=20in=20the=20?= =?UTF-8?q?lifetime=20set=20that=20is=20in=20a=20bot=E2=80=99s=20shadow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pelita/game.py | 6 +----- pelita/gamestate_filters.py | 10 +++++++--- test/test_filter_gamestates.py | 28 ++++++++++++---------------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index e8fa59068..e8ca7e8d5 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -297,11 +297,7 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= raise ValueError(f"Bot {idx} is not inside the layout: {pos}.") food = split_food(width, layout_dict['food']) - food_lifetime = {} max_food_lifetime = math.inf if allow_camping else MAX_FOOD_LIFETIME - for f_team in food: - for food_item in f_team: - food_lifetime[food_item] = max_food_lifetime # warn if one of the food lists is already empty side_no_food = [idx for idx, f in enumerate(food) if len(f) == 0] @@ -324,7 +320,7 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= food=food, #: Food lifetimes - food_lifetime=food_lifetime, + food_lifetime={}, #: Max food lifetime max_food_lifetime=max_food_lifetime, diff --git a/pelita/gamestate_filters.py b/pelita/gamestate_filters.py index 96c980870..20cfbe71a 100644 --- a/pelita/gamestate_filters.py +++ b/pelita/gamestate_filters.py @@ -137,9 +137,13 @@ def update_food_lifetimes(game_state, team, radius, max_food_lifetime=None): for pellet in food: if any(manhattan_dist(ghost, pellet) <= radius for ghost in ghosts): - food_lifetime[pellet] -= 1 + if pellet in food_lifetime: + food_lifetime[pellet] -= 1 + else: + food_lifetime[pellet] = max_food_lifetime - 1 else: - food_lifetime[pellet] = max_food_lifetime + if pellet in food_lifetime: + del food_lifetime[pellet] return {'food_lifetime': food_lifetime} @@ -176,7 +180,7 @@ def relocate_expired_food(game_state, team, radius, max_food_lifetime=None): # now convert to a list and sort, so that we have reproducibility (sets are unordered) targets = sorted(list(targets)) for pellet in sorted(list(food[team])): - if food_lifetime[pellet] > 0: + if pellet not in food_lifetime or food_lifetime[pellet] > 0: # the current pellet is fine, keep it! continue if not targets: diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index 7c7ccd796..83e4bdd5d 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -598,7 +598,7 @@ def test_update_food_lifetimes(): ################## """) mx = 60 parsed = parse_layout(test_layout) - food_lifetime = {pos: mx for pos in parsed['food']} + food_lifetime = {} food = split_food(parsed['shape'][0], parsed['food']) parsed.update({ @@ -607,26 +607,23 @@ def test_update_food_lifetimes(): }) radius = 1 - expected = {(3, 1): mx, (6, 1): mx, (6, 3): mx, # team0 - (11, 1): mx, (11, 3): mx, (14, 3): mx} # team1 + expected = {} # nothing should change for either team, the radius is too small assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected radius = 2 - expected_team0 = {(3, 1): mx-1, (6, 1): mx, (6, 3): mx, # team0 - (11, 1): mx, (11, 3): mx, (14, 3): mx} # team1 - expected_team1 = {(3, 1): mx, (6, 1): mx, (6, 3): mx, # team0 - (11, 1): mx, (11, 3): mx, (14, 3): mx-1} # team1 + expected_team0 = {(3, 1): mx-1 # team0 + } # team1 + expected_team1 = { # team0 + (14, 3): mx-1} # team1 # the two teams should get exactly one pellet updated assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected_team0 assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected_team1 radius = 4 - expected_team0 = {(3, 1): mx-1, (6, 1): mx, (6, 3): mx-1, # team0 - (11, 1): mx, (11, 3): mx, (14, 3): mx} # team1 - expected_team1 = {(3, 1): mx, (6, 1): mx, (6, 3): mx, # team0 - (11, 1): mx, (11, 3): mx, (14, 3): mx-1} # team1 + expected_team0 = {(3, 1): mx-1, (6, 3): mx-1} # team0 + expected_team1 = {(14, 3): mx-1} # team1 assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected_team0 assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected_team1 @@ -714,7 +711,6 @@ def test_relocate_expired_food_nospaceleft(): # now make space for the food and check that it gets located in the free spot parsed.update(out) parsed['food'][0].remove((7,1)) - del parsed['food_lifetime'][(7,1)] out = gf.relocate_expired_food(parsed, 0, radius, mx) assert to_relocate not in out['food'][0] assert (7, 1) in out['food'][0] @@ -735,10 +731,10 @@ def test_pacman_resets_lifetime(): parsed = parse_layout(test_layout) state = setup_game([team1, team2], layout_dict=parsed, max_rounds=8) - assert state['food_lifetime'] == {(8, 2): 30, (9, 2): 30} + assert state['food_lifetime'] == {} state = play_turn(state) assert state['turn'] == 0 - assert state['food_lifetime'] == {(8, 2): 29, (9, 2): 30} + assert state['food_lifetime'] == {(8, 2): 29} state = play_turn(state) assert state['turn'] == 1 assert state['food_lifetime'] == {(8, 2): 29, (9, 2): 29} @@ -753,7 +749,7 @@ def test_pacman_resets_lifetime(): assert state['food_lifetime'] == {(8, 2): 27, (9, 2): 27} state = play_turn(state) assert state['turn'] == 2 - assert state['food_lifetime'] == {(8, 2): 30, (9, 2): 27} + assert state['food_lifetime'] == {(9, 2): 27} state = play_turn(state) assert state['turn'] == 3 - assert state['food_lifetime'] == {(8, 2): 30, (9, 2): 30} + assert state['food_lifetime'] == {} From 542835d804de014061a562d443a482c8e21b9aa4 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Sun, 4 Aug 2024 23:58:44 +0200 Subject: [PATCH 17/28] RF: Split food_lifetime per team --- pelita/game.py | 11 +++++---- pelita/gamestate_filters.py | 20 ++++++++-------- test/test_filter_gamestates.py | 42 +++++++++++++++++----------------- 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index e8ca7e8d5..5a35bdfeb 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -319,8 +319,8 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= #: Food per team. List of sets of (int, int) food=food, - #: Food lifetimes - food_lifetime={}, + #: Food lifetimes per team. Dict of (int, int) to int + food_lifetime=[{}, {}], #: Max food lifetime max_food_lifetime=max_food_lifetime, @@ -626,7 +626,12 @@ def prepare_viewer_state(game_state): """ viewer_state = {} viewer_state.update(game_state) + + # Flatten food and food_lifetime viewer_state['food'] = list((viewer_state['food'][0] | viewer_state['food'][1])) + # We must transform the food lifetime dict to a list or we cannot serialise it + viewer_state['food_lifetime'] = [item for team_lifetime in viewer_state['food_lifetime'] + for item in team_lifetime.items()] # game_state["errors"] has a tuple as a dict key # that cannot be serialized in json. @@ -654,8 +659,6 @@ def prepare_viewer_state(game_state): del viewer_state['viewers'] del viewer_state['controller'] - # We must transform the food lifetime dict to a list or we cannot serialise it - viewer_state['food_lifetime'] = list(viewer_state['food_lifetime'].items()) return viewer_state diff --git a/pelita/gamestate_filters.py b/pelita/gamestate_filters.py index 20cfbe71a..c10cee1b1 100644 --- a/pelita/gamestate_filters.py +++ b/pelita/gamestate_filters.py @@ -131,19 +131,19 @@ def update_food_lifetimes(game_state, team, radius, max_food_lifetime=None): if in_homezone(bot, team, game_state['shape']) ] food = game_state['food'][team] - food_lifetime = dict(game_state['food_lifetime']) + food_lifetime = [dict(team_lifetime) for team_lifetime in game_state['food_lifetime']] if max_food_lifetime is None: max_food_lifetime = game_state['max_food_lifetime'] for pellet in food: if any(manhattan_dist(ghost, pellet) <= radius for ghost in ghosts): - if pellet in food_lifetime: - food_lifetime[pellet] -= 1 + if pellet in food_lifetime[team]: + food_lifetime[team][pellet] -= 1 else: - food_lifetime[pellet] = max_food_lifetime - 1 + food_lifetime[team][pellet] = max_food_lifetime - 1 else: - if pellet in food_lifetime: - del food_lifetime[pellet] + if pellet in food_lifetime[team]: + del food_lifetime[team][pellet] return {'food_lifetime': food_lifetime} @@ -152,7 +152,7 @@ def relocate_expired_food(game_state, team, radius, max_food_lifetime=None): bots = game_state['bots'][team::2] enemy_bots = game_state['bots'][1-team::2] food = [set(team_food) for team_food in game_state['food']] - food_lifetime = dict(game_state['food_lifetime']) + food_lifetime = [dict(team_lifetime) for team_lifetime in game_state['food_lifetime']] width, height = game_state['shape'] walls = game_state['walls'] rnd = game_state['rnd'] @@ -180,7 +180,7 @@ def relocate_expired_food(game_state, team, radius, max_food_lifetime=None): # now convert to a list and sort, so that we have reproducibility (sets are unordered) targets = sorted(list(targets)) for pellet in sorted(list(food[team])): - if pellet not in food_lifetime or food_lifetime[pellet] > 0: + if pellet not in food_lifetime[team] or food_lifetime[team][pellet] > 0: # the current pellet is fine, keep it! continue if not targets: @@ -196,10 +196,10 @@ def relocate_expired_food(game_state, team, radius, max_food_lifetime=None): # get rid of the old pellet food[team].remove(pellet) - del food_lifetime[pellet] + del food_lifetime[team][pellet] # track the new pellet - food_lifetime[new_pos] = max_food_lifetime + food_lifetime[team][new_pos] = max_food_lifetime food[team].add(new_pos) return {'food' : food, 'food_lifetime' : food_lifetime} diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index 83e4bdd5d..14debf0fe 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -598,8 +598,8 @@ def test_update_food_lifetimes(): ################## """) mx = 60 parsed = parse_layout(test_layout) - food_lifetime = {} food = split_food(parsed['shape'][0], parsed['food']) + food_lifetime = [{}, {}] parsed.update({ "food": food, @@ -607,23 +607,23 @@ def test_update_food_lifetimes(): }) radius = 1 - expected = {} + expected = [{}, {}] # nothing should change for either team, the radius is too small assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected radius = 2 - expected_team0 = {(3, 1): mx-1 # team0 - } # team1 - expected_team1 = { # team0 - (14, 3): mx-1} # team1 + expected_team0 = [{(3, 1): mx-1}, # team0 + {}] # team1 + expected_team1 = [{}, # team0 + {(14, 3): mx-1}] # team1 # the two teams should get exactly one pellet updated assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected_team0 assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected_team1 radius = 4 - expected_team0 = {(3, 1): mx-1, (6, 3): mx-1} # team0 - expected_team1 = {(14, 3): mx-1} # team1 + expected_team0 = [{(3, 1): mx-1, (6, 3): mx-1}, {}] # team0 + expected_team1 = [{}, {(14, 3): mx-1}] # team1 assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected_team0 assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected_team1 @@ -641,8 +641,8 @@ def test_relocate_expired_food(dummy): for team in (0, 1): mx = 1 parsed = parse_layout(test_layout) - food_lifetime = {pos: mx for pos in parsed['food']} food = split_food(parsed['shape'][0], parsed['food']) + food_lifetime = [{pos: mx for pos in team_food} for team_food in food] parsed.update({ "food": food, @@ -688,9 +688,9 @@ def test_relocate_expired_food_nospaceleft(): to_relocate = (3, 1) mx = 1 parsed = parse_layout(test_layout) - food_lifetime = {pos: mx for pos in parsed['food']} - food_lifetime[to_relocate] = 0 # set to zero so that we also test that we support negative lifetimes food = split_food(parsed['shape'][0], parsed['food']) + food_lifetime = [{pos: mx for pos in team_food} for team_food in food] + food_lifetime[0][to_relocate] = 0 # set to zero so that we also test that we support negative lifetimes parsed.update({ "food": food, @@ -706,7 +706,7 @@ def test_relocate_expired_food_nospaceleft(): # anywhere assert to_relocate in out['food'][0] assert len(out['food'][0]) == 7 - assert out['food_lifetime'][to_relocate] == -1 + assert out['food_lifetime'][0][to_relocate] == -1 # now make space for the food and check that it gets located in the free spot parsed.update(out) @@ -714,8 +714,8 @@ def test_relocate_expired_food_nospaceleft(): out = gf.relocate_expired_food(parsed, 0, radius, mx) assert to_relocate not in out['food'][0] assert (7, 1) in out['food'][0] - assert to_relocate not in out['food_lifetime'] - assert out['food_lifetime'][(7, 1)] == mx + assert to_relocate not in out['food_lifetime'][0] + assert out['food_lifetime'][0][(7, 1)] == mx def test_pacman_resets_lifetime(): # We move bot a across the border @@ -731,25 +731,25 @@ def test_pacman_resets_lifetime(): parsed = parse_layout(test_layout) state = setup_game([team1, team2], layout_dict=parsed, max_rounds=8) - assert state['food_lifetime'] == {} + assert state['food_lifetime'] == [{}, {}] state = play_turn(state) assert state['turn'] == 0 - assert state['food_lifetime'] == {(8, 2): 29} + assert state['food_lifetime'] == [{(8, 2): 29}, {}] state = play_turn(state) assert state['turn'] == 1 - assert state['food_lifetime'] == {(8, 2): 29, (9, 2): 29} + assert state['food_lifetime'] == [{(8, 2): 29}, {(9, 2): 29}] state = play_turn(state) state = play_turn(state) state = play_turn(state) # NOTE: food_lifetimes are calculated *before* the move # Therefore this will only be updated once it is team1’s turn again assert state['turn'] == 0 - assert state['food_lifetime'] == {(8, 2): 27, (9, 2): 28} + assert state['food_lifetime'] == [{(8, 2): 27}, {(9, 2): 28}] state = play_turn(state) - assert state['food_lifetime'] == {(8, 2): 27, (9, 2): 27} + assert state['food_lifetime'] == [{(8, 2): 27}, {(9, 2): 27}] state = play_turn(state) assert state['turn'] == 2 - assert state['food_lifetime'] == {(9, 2): 27} + assert state['food_lifetime'] == [{}, {(9, 2): 27}] state = play_turn(state) assert state['turn'] == 3 - assert state['food_lifetime'] == {} + assert state['food_lifetime'] == [{}, {}] From b7ec80af43634c278fac3b415196efc217e334eb Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Mon, 5 Aug 2024 14:39:26 +0200 Subject: [PATCH 18/28] RF: Rename food_lifetime to food_age and start counting from 0 --- pelita/game.py | 34 ++++++------- pelita/gamestate_filters.py | 71 +++++++++++++-------------- pelita/scripts/pelita_main.py | 6 +-- pelita/ui/tk_canvas.py | 10 ++-- pelita/ui/tk_sprites.py | 24 +++++----- test/test_filter_gamestates.py | 88 ++++++++++++++++++---------------- 6 files changed, 119 insertions(+), 114 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index 5a35bdfeb..29a49286b 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -11,7 +11,7 @@ from . import layout from .exceptions import FatalException, NonFatalException, NoFoodWarning, PlayerTimeout -from .gamestate_filters import noiser, update_food_lifetimes, relocate_expired_food +from .gamestate_filters import noiser, update_food_age, relocate_expired_food from .layout import initial_positions, get_legal_positions from .network import setup_controller, ZMQPublisher from .team import make_team @@ -34,10 +34,10 @@ NOISE_RADIUS = 5 #: The lifetime of food pellets in a shadow in turns -MAX_FOOD_LIFETIME = 30 +MAX_FOOD_AGE = 30 -#: Food pellet lifetime distance -LIFETIME_DISTANCE = 3 +#: Food pellet shadow distance +SHADOW_DISTANCE = 3 class TkViewer: def __init__(self, *, address, controller, geometry=None, delay=None, stop_after=None): @@ -282,7 +282,7 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= raise ValueError("Number of bots in layout must be 4.") width, height = layout.wall_dimensions(layout_dict['walls']) - if not (width, height) == layout_dict['shape']: + if not (width, height) == layout_dict["shape"]: raise ValueError(f"layout_dict['walls'] does not match layout_dict['shape'].") for idx, pos in enumerate(layout_dict['bots']): @@ -297,7 +297,7 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= raise ValueError(f"Bot {idx} is not inside the layout: {pos}.") food = split_food(width, layout_dict['food']) - max_food_lifetime = math.inf if allow_camping else MAX_FOOD_LIFETIME + max_food_age = math.inf if allow_camping else MAX_FOOD_AGE # warn if one of the food lists is already empty side_no_food = [idx for idx, f in enumerate(food) if len(f) == 0] @@ -319,11 +319,11 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= #: Food per team. List of sets of (int, int) food=food, - #: Food lifetimes per team. Dict of (int, int) to int - food_lifetime=[{}, {}], + #: Food ages per team. Dict of (int, int) to int + food_age=[{}, {}], - #: Max food lifetime - max_food_lifetime=max_food_lifetime, + #: Max food age + max_food_age=max_food_age, ### Round/turn information #: Current bot, int, None @@ -627,11 +627,11 @@ def prepare_viewer_state(game_state): viewer_state = {} viewer_state.update(game_state) - # Flatten food and food_lifetime + # Flatten food and food_age viewer_state['food'] = list((viewer_state['food'][0] | viewer_state['food'][1])) - # We must transform the food lifetime dict to a list or we cannot serialise it - viewer_state['food_lifetime'] = [item for team_lifetime in viewer_state['food_lifetime'] - for item in team_lifetime.items()] + # We must transform the food age dict to a list or we cannot serialise it + viewer_state['food_age'] = [item for team_food_age in viewer_state['food_age'] + for item in team_food_age.items()] # game_state["errors"] has a tuple as a dict key # that cannot be serialized in json. @@ -686,9 +686,9 @@ def play_turn(game_state, allow_exceptions=False): round = game_state['round'] team = turn % 2 - # update food lifetimes and relocate expired food for the current team - game_state.update(update_food_lifetimes(game_state, team, LIFETIME_DISTANCE)) - game_state.update(relocate_expired_food(game_state, team, LIFETIME_DISTANCE)) + # update food age and relocate expired food for the current team + game_state.update(update_food_age(game_state, team, SHADOW_DISTANCE)) + game_state.update(relocate_expired_food(game_state, team, SHADOW_DISTANCE)) # request a new move from the current team try: diff --git a/pelita/gamestate_filters.py b/pelita/gamestate_filters.py index c10cee1b1..965a641b7 100644 --- a/pelita/gamestate_filters.py +++ b/pelita/gamestate_filters.py @@ -124,40 +124,38 @@ def in_homezone(position, team_id, shape): return position[0] >= boundary -def update_food_lifetimes(game_state, team, radius, max_food_lifetime=None): +def update_food_age(game_state, team, radius): # Only ghosts can cast a shadow ghosts = [ bot for bot in game_state['bots'][team::2] if in_homezone(bot, team, game_state['shape']) ] food = game_state['food'][team] - food_lifetime = [dict(team_lifetime) for team_lifetime in game_state['food_lifetime']] - if max_food_lifetime is None: - max_food_lifetime = game_state['max_food_lifetime'] + food_age = [dict(team_food_age) for team_food_age in game_state['food_age']] for pellet in food: if any(manhattan_dist(ghost, pellet) <= radius for ghost in ghosts): - if pellet in food_lifetime[team]: - food_lifetime[team][pellet] -= 1 + if pellet in food_age[team]: + food_age[team][pellet] += 1 else: - food_lifetime[team][pellet] = max_food_lifetime - 1 + food_age[team][pellet] = 1 else: - if pellet in food_lifetime[team]: - del food_lifetime[team][pellet] + if pellet in food_age[team]: + del food_age[team][pellet] - return {'food_lifetime': food_lifetime} + return {'food_age': food_age} -def relocate_expired_food(game_state, team, radius, max_food_lifetime=None): +def relocate_expired_food(game_state, team, radius, max_food_age=None): bots = game_state['bots'][team::2] enemy_bots = game_state['bots'][1-team::2] food = [set(team_food) for team_food in game_state['food']] - food_lifetime = [dict(team_lifetime) for team_lifetime in game_state['food_lifetime']] + food_age = [dict(team_food_age) for team_food_age in game_state['food_age']] width, height = game_state['shape'] walls = game_state['walls'] rnd = game_state['rnd'] - if max_food_lifetime is None: - max_food_lifetime = game_state['max_food_lifetime'] + if max_food_age is None: + max_food_age = game_state['max_food_age'] # generate a set of possible positions to relocate food: # - in the bot's homezone @@ -180,29 +178,28 @@ def relocate_expired_food(game_state, team, radius, max_food_lifetime=None): # now convert to a list and sort, so that we have reproducibility (sets are unordered) targets = sorted(list(targets)) for pellet in sorted(list(food[team])): - if pellet not in food_lifetime[team] or food_lifetime[team][pellet] > 0: - # the current pellet is fine, keep it! - continue - if not targets: - # we have no free positions anymore, just let the food stay where it is - # we do not update the lifetime, so this pellet will get a chance to be - # relocated at the next round - continue - # choose a new position at random - new_pos = rnd.choice(targets) - - # remove the new pellet position from the list of possible targets for new pellets - targets.remove(new_pos) - - # get rid of the old pellet - food[team].remove(pellet) - del food_lifetime[team][pellet] - - # track the new pellet - food_lifetime[team][new_pos] = max_food_lifetime - food[team].add(new_pos) - - return {'food' : food, 'food_lifetime' : food_lifetime} + # We move the pellet if it is in the food_age dict and exceeds the max_food_age + if food_age[team].get(pellet, 0) >= max_food_age: + if not targets: + # we have no free positions anymore, just let the food stay where it is + # we do not update the age, so this pellet will get a chance to be + # relocated at the next round + continue + # choose a new position at random + new_pos = rnd.choice(targets) + + # remove the new pellet position from the list of possible targets for new pellets + targets.remove(new_pos) + + # get rid of the old pellet + food[team].remove(pellet) + del food_age[team][pellet] + + # add the new pellet to food again + # (starts with 0 food age, so we do not need to add it to the food_age dict) + food[team].add(new_pos) + + return {'food' : food, 'food_age' : food_age} def manhattan_dist(pos1, pos2): diff --git a/pelita/scripts/pelita_main.py b/pelita/scripts/pelita_main.py index 71a44037a..174062284 100755 --- a/pelita/scripts/pelita_main.py +++ b/pelita/scripts/pelita_main.py @@ -221,8 +221,8 @@ def long_help(s): help='Maximum number of rounds to play.') game_settings.add_argument('--seed', type=int, metavar='SEED', default=None, help='Initialize the random number generator with SEED.') -game_settings.add_argument('--allow-squatting', type=bool, default=False, - help='do not set a food lifetime') +game_settings.add_argument('--allow-camping', const=True, action='store_const', + help='Food does not age when in a bot’s shadow') layout_opt = game_settings.add_mutually_exclusive_group() layout_opt.add_argument('--layout', metavar='LAYOUT', @@ -450,7 +450,7 @@ def main(): layout_dict = pelita.layout.parse_layout(layout_string) pelita.game.run_game(team_specs=team_specs, max_rounds=args.rounds, layout_dict=layout_dict, layout_name=layout_name, seed=seed, - timeout_length=args.timeout_length, error_limit=args.error_limit, + allow_camping=args.allow_camping, timeout_length=args.timeout_length, error_limit=args.error_limit, viewers=viewers, viewer_options=viewer_options, store_output=args.store_output, team_infos=(args.append_blue, args.append_red)) diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index ffbd8424a..8d1498e35 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -413,8 +413,8 @@ def update(self, game_state=None): eaten_food = [] for food_pos, food_item in self.food_items.items(): - food_item.food_lifetime = game_state['food_lifetime'][food_pos] - if not food_pos in game_state["food"]: + food_item.food_age = game_state['food_age'].get(food_pos, 0) + if food_pos not in game_state["food"]: self.ui.game_canvas.delete(food_item.tag) eaten_food.append(food_pos) for food_pos in eaten_food: @@ -796,8 +796,8 @@ def draw_food(self, game_state): self.food_items = {} for position in game_state['food']: model_x, model_y = position - lifetime = game_state['food_lifetime'][position] - food_item = Food(self.mesh_graph, position=(model_x, model_y), food_lifetime=lifetime) + food_age = game_state['food_age'].get(position, 0) + food_item = Food(self.mesh_graph, position=(model_x, model_y), food_age=food_age) food_item.draw(self.ui.game_canvas) self.food_items[position] = food_item @@ -969,7 +969,7 @@ def observe(self, game_state): game_state['food'] = _ensure_list_tuples(game_state['food']) game_state['bots'] = _ensure_list_tuples(game_state['bots']) game_state['shape'] = tuple(game_state['shape']) - game_state['food_lifetime'] = {tuple(pos): lifetime for pos, lifetime in game_state['food_lifetime']} + game_state['food_age'] = {tuple(pos): food_age for pos, food_age in game_state['food_age']} self.update(game_state) if self._stop_after is not None: if self._stop_after == 0: diff --git a/pelita/ui/tk_sprites.py b/pelita/ui/tk_sprites.py index 5baac4231..74e610939 100644 --- a/pelita/ui/tk_sprites.py +++ b/pelita/ui/tk_sprites.py @@ -294,8 +294,8 @@ def draw(self, canvas, game_state=None): class Food(TkSprite): - def __init__(self, mesh, food_lifetime=None, **kwargs): - self.food_lifetime = food_lifetime + def __init__(self, mesh, food_age=None, **kwargs): + self.food_age = food_age super().__init__(mesh, **kwargs) @classmethod @@ -309,18 +309,20 @@ def draw(self, canvas, game_state=None): fill = RED canvas.create_oval(self.bounding_box(0.4), fill=fill, width=0, tag=(self.tag, self.food_pos_tag(self.position), "food")) - canvas.delete("show_lifetime" + str(self.position)) - lifetime = self.food_lifetime + food_age = self.food_age + + canvas.delete("show_food_age" + str(self.position)) + # we print the bot_id in the lower left corner - if self.food_lifetime: + if self.food_age: shift_x = 32 shift_y = 16 - tag=(self.tag, "show_lifetime" + str(self.position), "food") - canvas.create_text(self.bounding_box()[0][0]-1 + shift_x, self.bounding_box()[1][1] - shift_y, text=lifetime, font=(None, 12), fill="white", tag=tag) - canvas.create_text(self.bounding_box()[0][0]+1 + shift_x, self.bounding_box()[1][1] - shift_y, text=lifetime, font=(None, 12), fill="white", tag=tag) - canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1]-1 - shift_y, text=lifetime, font=(None, 12), fill="white", tag=tag) - canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1]+1 - shift_y, text=lifetime, font=(None, 12), fill="white", tag=tag) - canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1] - shift_y, text=lifetime, font=(None, 12), fill="black", tag=tag) + tag=(self.tag, "show_food_age" + str(self.position), "food") + canvas.create_text(self.bounding_box()[0][0]-1 + shift_x, self.bounding_box()[1][1] - shift_y, text=food_age, font=(None, 12), fill="white", tag=tag) + canvas.create_text(self.bounding_box()[0][0]+1 + shift_x, self.bounding_box()[1][1] - shift_y, text=food_age, font=(None, 12), fill="white", tag=tag) + canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1]-1 - shift_y, text=food_age, font=(None, 12), fill="white", tag=tag) + canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1]+1 - shift_y, text=food_age, font=(None, 12), fill="white", tag=tag) + canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1] - shift_y, text=food_age, font=(None, 12), fill="black", tag=tag) class Arrow(TkSprite): diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index 14debf0fe..87dd1345d 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -589,43 +589,42 @@ def test_noise_manhattan_failure(): noised_pos = noised['enemy_positions'] assert noised_pos == parsed['bots'][0::2] -def test_update_food_lifetimes(): +def test_update_food_ages(): test_layout = ( """ ################## # #. . # . b # # ##### #####y# # a . # . .#x# ################## """) - mx = 60 parsed = parse_layout(test_layout) food = split_food(parsed['shape'][0], parsed['food']) - food_lifetime = [{}, {}] + food_age = [{}, {}] parsed.update({ "food": food, - "food_lifetime": food_lifetime, + "food_age": food_age, }) radius = 1 expected = [{}, {}] # nothing should change for either team, the radius is too small - assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected - assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected + assert gf.update_food_age(parsed, 0, radius)['food_age'] == expected + assert gf.update_food_age(parsed, 1, radius)['food_age'] == expected radius = 2 - expected_team0 = [{(3, 1): mx-1}, # team0 - {}] # team1 - expected_team1 = [{}, # team0 - {(14, 3): mx-1}] # team1 + expected_team0 = [{(3, 1): 1}, # team0 + {}] # team1 + expected_team1 = [{}, # team0 + {(14, 3): 1}] # team1 # the two teams should get exactly one pellet updated - assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected_team0 - assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected_team1 + assert gf.update_food_age(parsed, 0, radius)['food_age'] == expected_team0 + assert gf.update_food_age(parsed, 1, radius)['food_age'] == expected_team1 radius = 4 - expected_team0 = [{(3, 1): mx-1, (6, 3): mx-1}, {}] # team0 - expected_team1 = [{}, {(14, 3): mx-1}] # team1 - assert gf.update_food_lifetimes(parsed, 0, radius, mx)['food_lifetime'] == expected_team0 - assert gf.update_food_lifetimes(parsed, 1, radius, mx)['food_lifetime'] == expected_team1 + expected_team0 = [{(3, 1): 1, (6, 3): 1}, {}] # team0 + expected_team1 = [{}, {(14, 3): 1}] # team1 + assert gf.update_food_age(parsed, 0, radius)['food_age'] == expected_team0 + assert gf.update_food_age(parsed, 1, radius)['food_age'] == expected_team1 # repeat the test 20 times to exercise the randomness of the relocation algorithm @pytest.mark.parametrize('dummy', range(20)) @@ -642,17 +641,17 @@ def test_relocate_expired_food(dummy): mx = 1 parsed = parse_layout(test_layout) food = split_food(parsed['shape'][0], parsed['food']) - food_lifetime = [{pos: mx for pos in team_food} for team_food in food] + food_age = [{pos: mx for pos in team_food} for team_food in food] parsed.update({ "food": food, - "food_lifetime": food_lifetime, + "food_age": food_age, "rnd" : random.Random(), }) radius = 2 - parsed.update(gf.update_food_lifetimes(parsed, team, radius, mx)) + parsed.update(gf.update_food_age(parsed, team, radius)) out = gf.relocate_expired_food(parsed, team, radius, mx) # check that the expired pellet is gone, bot only when it's our team turn @@ -685,41 +684,48 @@ def test_relocate_expired_food_nospaceleft(): #######y ###### ###a##..# . .#x# ################## """) + + # location of the food pellet that will be relocated to_relocate = (3, 1) - mx = 1 + # location of the pellet that will be removed + to_remove = (7, 1) + max_age = 2 + parsed = parse_layout(test_layout) food = split_food(parsed['shape'][0], parsed['food']) - food_lifetime = [{pos: mx for pos in team_food} for team_food in food] - food_lifetime[0][to_relocate] = 0 # set to zero so that we also test that we support negative lifetimes + food_age = [{}, {}] + food_age[0][to_relocate] = 1 parsed.update({ "food": food, - "food_lifetime": food_lifetime, + "food_age": food_age, "rnd" : random.Random(), }) radius = 2 - parsed.update(gf.update_food_lifetimes(parsed, 0, radius, mx)) - out = gf.relocate_expired_food(parsed, 0, radius, mx) + parsed.update(gf.update_food_age(parsed, 0, radius)) + out = gf.relocate_expired_food(parsed, 0, radius, max_age) # check that the food pellet did not move: there is no space to move it # anywhere assert to_relocate in out['food'][0] assert len(out['food'][0]) == 7 - assert out['food_lifetime'][0][to_relocate] == -1 + # check that food age has increased + assert out['food_age'][0] == {to_relocate: 2} # now make space for the food and check that it gets located in the free spot parsed.update(out) - parsed['food'][0].remove((7,1)) - out = gf.relocate_expired_food(parsed, 0, radius, mx) + parsed['food'][0].remove(to_remove) + out = gf.relocate_expired_food(parsed, 0, radius, max_age) assert to_relocate not in out['food'][0] - assert (7, 1) in out['food'][0] - assert to_relocate not in out['food_lifetime'][0] - assert out['food_lifetime'][0][(7, 1)] == mx + assert to_remove in out['food'][0] + # Both the relocated and the moved to position should have their ages reset + assert to_relocate not in out['food_age'][0] + assert to_remove not in out['food_age'][0] -def test_pacman_resets_lifetime(): +def test_pacman_resets_age(): # We move bot a across the border - # Once it becomes a bot, the food_lifetime should reset + # Once it becomes a bot, the food_age should reset test_layout = ( """ ################## # x y# @@ -731,25 +737,25 @@ def test_pacman_resets_lifetime(): parsed = parse_layout(test_layout) state = setup_game([team1, team2], layout_dict=parsed, max_rounds=8) - assert state['food_lifetime'] == [{}, {}] + assert state['food_age'] == [{}, {}] state = play_turn(state) assert state['turn'] == 0 - assert state['food_lifetime'] == [{(8, 2): 29}, {}] + assert state['food_age'] == [{(8, 2): 1}, {}] state = play_turn(state) assert state['turn'] == 1 - assert state['food_lifetime'] == [{(8, 2): 29}, {(9, 2): 29}] + assert state['food_age'] == [{(8, 2): 1}, {(9, 2): 1}] state = play_turn(state) state = play_turn(state) state = play_turn(state) - # NOTE: food_lifetimes are calculated *before* the move + # NOTE: food_ages are calculated *before* the move # Therefore this will only be updated once it is team1’s turn again assert state['turn'] == 0 - assert state['food_lifetime'] == [{(8, 2): 27}, {(9, 2): 28}] + assert state['food_age'] == [{(8, 2): 3}, {(9, 2): 2}] state = play_turn(state) - assert state['food_lifetime'] == [{(8, 2): 27}, {(9, 2): 27}] + assert state['food_age'] == [{(8, 2): 3}, {(9, 2): 3}] state = play_turn(state) assert state['turn'] == 2 - assert state['food_lifetime'] == [{}, {(9, 2): 27}] + assert state['food_age'] == [{}, {(9, 2): 3}] state = play_turn(state) assert state['turn'] == 3 - assert state['food_lifetime'] == [{}, {}] + assert state['food_age'] == [{}, {}] From c6c13085636ac4ef928fef3d54f6bad0cee6ca9f Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Mon, 5 Aug 2024 15:55:42 +0200 Subject: [PATCH 19/28] ENH: Add shaded_food to Bot --- pelita/game.py | 4 ++++ pelita/team.py | 4 ++++ pelita/utils.py | 13 ++++++++++++- test/test_network.py | 2 ++ test/test_team.py | 6 ++++-- 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index 29a49286b..6f9ab24d7 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -557,6 +557,8 @@ def prepare_bot_state(game_state, idx=None): zip(noised_positions['is_noisy'], noised_positions['enemy_positions']) ] game_state['noisy_positions'][enemy_team::2] = noisy_or_none + shaded_food = list(pos for pos, age in game_state['food_age'][own_team].items() + if age > 0) team_state = { 'team_index': own_team, @@ -567,6 +569,7 @@ def prepare_bot_state(game_state, idx=None): 'bot_was_killed': game_state['bot_was_killed'][own_team::2], 'error_count': len(game_state['errors'][own_team]), 'food': list(game_state['food'][own_team]), + 'shaded_food': shaded_food, 'name': game_state['team_names'][own_team], 'team_time': game_state['team_time'][own_team] } @@ -581,6 +584,7 @@ def prepare_bot_state(game_state, idx=None): 'bot_was_killed': game_state['bot_was_killed'][enemy_team::2], 'error_count': 0, # TODO. Could be left out for the enemy 'food': list(game_state['food'][enemy_team]), + 'shaded_food': [], 'name': game_state['team_names'][enemy_team], 'team_time': game_state['team_time'][enemy_team] } diff --git a/pelita/team.py b/pelita/team.py index 02c66bca7..8a1745be0 100644 --- a/pelita/team.py +++ b/pelita/team.py @@ -512,6 +512,7 @@ def __init__(self, *, bot_index, shape, homezone, food, + shaded_food, score, kills, deaths, @@ -542,6 +543,7 @@ def __init__(self, *, bot_index, self.homezone = homezone self.food = food + self.shaded_food = shaded_food self.shape = shape self.score = score self.kills = kills @@ -736,6 +738,7 @@ def make_bots(*, walls, shape, initial_positions, homezone, team, enemy, round, is_noisy=False, error_count=team['error_count'], food=_ensure_list_tuples(team['food']), + shaded_food=_ensure_list_tuples(team['shaded_food']), walls=walls, shape=shape, round=round, @@ -763,6 +766,7 @@ def make_bots(*, walls, shape, initial_positions, homezone, team, enemy, round, is_noisy=enemy['is_noisy'][idx], error_count=enemy['error_count'], food=_ensure_list_tuples(enemy['food']), + shaded_food=_ensure_list_tuples(team['shaded_food']), walls=walls, shape=shape, round=round, diff --git a/pelita/utils.py b/pelita/utils.py index ef44921b1..4f6eda041 100644 --- a/pelita/utils.py +++ b/pelita/utils.py @@ -4,9 +4,10 @@ from .team import make_bots, create_homezones -from .game import split_food +from .game import split_food, SHADOW_DISTANCE 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 # this import is needed for backward compatibility, do not remove or you'll break # older clients! @@ -153,6 +154,14 @@ def run_background_game(*, blue_move, red_move, layout=None, max_rounds=300, see return out +def shaded_food(pos, food, radius): + # Get all food that is in a radius around any of pos + # TODO: This duplicates code in update_food_age + for pellet in food: + if any(manhattan_dist(ghost, pellet) <= radius for ghost in pos): + yield pellet + + def setup_test_game(*, layout, is_blue=True, round=None, score=None, seed=None, food=None, bots=None, is_noisy=None): """Setup a test game environment useful for testing move functions. @@ -239,6 +248,7 @@ def setup_test_game(*, layout, is_blue=True, round=None, score=None, seed=None, 'bot_was_killed' : [False]*2, 'error_count': 0, 'food': food[team_index], + 'shaded_food': shaded_food(bot_positions, food[team_index], radius=SHADOW_DISTANCE), 'name': "blue" if is_blue else "red", 'team_time': 0.0, } @@ -251,6 +261,7 @@ def setup_test_game(*, layout, is_blue=True, round=None, score=None, seed=None, 'bot_was_killed': [False]*2, 'error_count': 0, 'food': food[enemy_index], + 'shaded_food': [], 'is_noisy': is_noisy_enemy, 'name': "red" if is_blue else "blue", 'team_time': 0.0, diff --git a/test/test_network.py b/test/test_network.py index 9fcaca7fb..251db134b 100644 --- a/test/test_network.py +++ b/test/test_network.py @@ -87,6 +87,7 @@ def stopping(bot, state): 'bot_was_killed': [False]*2, 'error_count': 0, 'food': [(1, 1)], + 'shaded_food': [(1, 1)], 'name': 'dummy', 'team_time': 0, }, @@ -98,6 +99,7 @@ def stopping(bot, state): 'deaths': [0]*2, 'bot_was_killed': [False]*2, 'food': [(2, 2)], + 'shaded_food': [], 'name': 'other dummy', 'team_time': 0, 'is_noisy': [False, False], diff --git a/test/test_team.py b/test/test_team.py index df0ff3940..1383d0cad 100644 --- a/test/test_team.py +++ b/test/test_team.py @@ -366,10 +366,10 @@ def test_bot_attributes(): test_layout = """ ################## #.#... .##. y# - # # # . .### #x# + # #a# . .### #x# # ####. . # # . .#### # - #a# ###. . # # # + # # ###. . # # # #b .##. ...#.# ################## """ @@ -399,9 +399,11 @@ def asserting_team(bot, state): if bot.is_blue: assert set(bot.homezone) == set(homezones[0]) assert set(bot.enemy[0].homezone) == set(homezones[1]) + assert set(bot.shaded_food) == set([(1, 1), (3, 1), (4, 1), (5, 1)]) else: assert set(bot.homezone) == set(homezones[1]) assert set(bot.enemy[0].homezone) == set(homezones[0]) + assert set(bot.shaded_food) == set() return bot.position state = run_game([asserting_team, asserting_team], max_rounds=1, layout_dict=parsed) From 1a9485e9835730fd1e31b4090c080fc519234a95 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Mon, 5 Aug 2024 16:12:16 +0200 Subject: [PATCH 20/28] BF: shaded_food needs to be a list --- pelita/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pelita/utils.py b/pelita/utils.py index 4f6eda041..d005dab18 100644 --- a/pelita/utils.py +++ b/pelita/utils.py @@ -248,7 +248,7 @@ def setup_test_game(*, layout, is_blue=True, round=None, score=None, seed=None, 'bot_was_killed' : [False]*2, 'error_count': 0, 'food': food[team_index], - 'shaded_food': shaded_food(bot_positions, food[team_index], radius=SHADOW_DISTANCE), + 'shaded_food': list(shaded_food(bot_positions, food[team_index], radius=SHADOW_DISTANCE)), 'name': "blue" if is_blue else "red", 'team_time': 0.0, } From f137884585cd41ea6e137aaff0ee88a8b402ec35 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Tue, 6 Aug 2024 10:21:26 +0200 Subject: [PATCH 21/28] TST: Add test for different radii --- pelita/game.py | 9 ++++--- test/test_filter_gamestates.py | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index 6f9ab24d7..deb6250e9 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -322,9 +322,6 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= #: Food ages per team. Dict of (int, int) to int food_age=[{}, {}], - #: Max food age - max_food_age=max_food_age, - ### Round/turn information #: Current bot, int, None turn=None, @@ -364,6 +361,12 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, layout_name="", seed= #: Sight distance, int sight_distance=SIGHT_DISTANCE, + #: Max food age + max_food_age=max_food_age, + + #: Shadow distance, int + shadow_distance=SHADOW_DISTANCE, + ### Informative #: Name of the layout, str layout_name=layout_name, diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index 87dd1345d..da35cda20 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -8,6 +8,7 @@ from pelita.game import setup_game, play_turn, prepare_bot_state, split_food from pelita.layout import parse_layout from pelita.player import stepping_player +import pelita.utils def make_gamestate(): @@ -626,6 +627,53 @@ def test_update_food_ages(): assert gf.update_food_age(parsed, 0, radius)['food_age'] == expected_team0 assert gf.update_food_age(parsed, 1, radius)['food_age'] == expected_team1 +@pytest.mark.parametrize('radius, team', [ + [0, 0], + [0, 1], + [1, 0], + [1, 1], + [2, 0], + [2, 1] +]) +def test_shadow_radius(team, radius): + test_layout = ( + """ ################## + #................# + #............y...# + #....a..........x# + #..b.............# + #................# + ################## """) + parsed = parse_layout(test_layout) + # We want to have food below each bot + parsed['food'].extend(parsed['bots']) + food = split_food(parsed['shape'][0], parsed['food']) + food_age = [{}, {}] + + parsed.update({ + "food": food, + "food_age": food_age, + }) + + check_food = [[ + {(3, 4), (5, 3)}, # radius 0 + {(4, 4), (2, 4), (4, 3), (5, 4), (3, 3), (6, 3), (3, 5), (5, 2)}, # radius 1 + {(6, 2), (5, 5), (5, 1), (4, 2), (6, 4), (1, 4), (7, 3), (2, 3), (4, 5), (3, 2), (2, 5)}, # radius 2 + ],[ + {(13, 2), (16, 3)}, # radius 0 + {(13, 1), (13, 3), (16, 2), (12, 2), (15, 3), (14, 2), (16, 4)}, # radius 1 + {(12, 1), (13, 4), (14, 1), (15, 4), (12, 3), (11, 2), (16, 5), (14, 3), (15, 2), (16, 1)}, # radius 3 + ]] + + # for each team we sum all foods for all radii below the given radius + shaded_food = set().union(*check_food[team][0:radius + 1]) + + # nothing should change for either team, the radius is too small + assert set(gf.update_food_age(parsed, team, radius)['food_age'][team].keys()) == shaded_food + # testing pelita.utils.shaded_food as well here + assert set(pelita.utils.shaded_food(parsed['bots'][team::2], parsed['food'][team], radius)) == shaded_food + + # repeat the test 20 times to exercise the randomness of the relocation algorithm @pytest.mark.parametrize('dummy', range(20)) def test_relocate_expired_food(dummy): From 8bed3b69e03e565335166c94f10fcd31cf7eb786 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Thu, 8 Aug 2024 10:29:45 +0200 Subject: [PATCH 22/28] ENH: Make food grey before it is about to expire --- pelita/ui/tk_canvas.py | 8 +++++++- pelita/ui/tk_sprites.py | 19 ++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index 8d1498e35..8eb822ac6 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -794,10 +794,16 @@ def draw_food(self, game_state): # return self.ui.game_canvas.delete("food") self.food_items = {} + max_food_age = game_state["max_food_age"] for position in game_state['food']: model_x, model_y = position food_age = game_state['food_age'].get(position, 0) - food_item = Food(self.mesh_graph, position=(model_x, model_y), food_age=food_age) + food_item = Food( + self.mesh_graph, + position=(model_x, model_y), + food_age=food_age, + max_food_age=max_food_age, + ) food_item.draw(self.ui.game_canvas) self.food_items[position] = food_item diff --git a/pelita/ui/tk_sprites.py b/pelita/ui/tk_sprites.py index 74e610939..bd3227177 100644 --- a/pelita/ui/tk_sprites.py +++ b/pelita/ui/tk_sprites.py @@ -24,6 +24,8 @@ def col(red, green, blue): SHADOW_RED = '#B37373' SHADOW_BLUE = '#6D92B3' +FOOD_WARNING_TIME = 5 + def rotate(arc, rotation): """Helper for rotation normalisation.""" return (arc + rotation) % 360 @@ -294,8 +296,12 @@ def draw(self, canvas, game_state=None): class Food(TkSprite): - def __init__(self, mesh, food_age=None, **kwargs): + def __init__(self, mesh, food_age=None, max_food_age=None, **kwargs): self.food_age = food_age + if max_food_age is None: + self.max_food_age = math.inf + else: + self.max_food_age = max_food_age super().__init__(mesh, **kwargs) @classmethod @@ -304,17 +310,20 @@ def food_pos_tag(cls, position): def draw(self, canvas, game_state=None): if self.position[0] < self.mesh.num_x/2: - fill = BLUE + fill_col = BLUE else: - fill = RED - canvas.create_oval(self.bounding_box(0.4), fill=fill, width=0, tag=(self.tag, self.food_pos_tag(self.position), "food")) + fill_col = RED food_age = self.food_age + if food_age and food_age + FOOD_WARNING_TIME > self.max_food_age: + fill_col = GREY + canvas.create_oval(self.bounding_box(0.4), fill=fill_col, width=0, tag=(self.tag, self.food_pos_tag(self.position), "food")) + canvas.delete("show_food_age" + str(self.position)) # we print the bot_id in the lower left corner - if self.food_age: + if food_age: shift_x = 32 shift_y = 16 tag=(self.tag, "show_food_age" + str(self.position), "food") From 7d1cbe370bad5c8bdeadfb7cccb76986d02941fa Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Thu, 8 Aug 2024 10:47:17 +0200 Subject: [PATCH 23/28] ENH: Show food lifetime on the pellet and only in grid (debug) mode --- pelita/ui/tk_canvas.py | 2 +- pelita/ui/tk_sprites.py | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index 8eb822ac6..653119638 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -804,7 +804,7 @@ def draw_food(self, game_state): food_age=food_age, max_food_age=max_food_age, ) - food_item.draw(self.ui.game_canvas) + food_item.draw(self.ui.game_canvas, show_lifetime=self._grid_enabled) self.food_items[position] = food_item def draw_maze(self, game_state): diff --git a/pelita/ui/tk_sprites.py b/pelita/ui/tk_sprites.py index bd3227177..e836ce1ae 100644 --- a/pelita/ui/tk_sprites.py +++ b/pelita/ui/tk_sprites.py @@ -308,31 +308,28 @@ def __init__(self, mesh, food_age=None, max_food_age=None, **kwargs): def food_pos_tag(cls, position): return "Food" + str(position) - def draw(self, canvas, game_state=None): + def draw(self, canvas, game_state=None, show_lifetime=False): if self.position[0] < self.mesh.num_x/2: fill_col = BLUE else: fill_col = RED + text_col = "#000" food_age = self.food_age if food_age and food_age + FOOD_WARNING_TIME > self.max_food_age: fill_col = GREY + text_col = YELLOW canvas.create_oval(self.bounding_box(0.4), fill=fill_col, width=0, tag=(self.tag, self.food_pos_tag(self.position), "food")) canvas.delete("show_food_age" + str(self.position)) # we print the bot_id in the lower left corner - if food_age: - shift_x = 32 - shift_y = 16 + if food_age and show_lifetime: tag=(self.tag, "show_food_age" + str(self.position), "food") - canvas.create_text(self.bounding_box()[0][0]-1 + shift_x, self.bounding_box()[1][1] - shift_y, text=food_age, font=(None, 12), fill="white", tag=tag) - canvas.create_text(self.bounding_box()[0][0]+1 + shift_x, self.bounding_box()[1][1] - shift_y, text=food_age, font=(None, 12), fill="white", tag=tag) - canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1]-1 - shift_y, text=food_age, font=(None, 12), fill="white", tag=tag) - canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1]+1 - shift_y, text=food_age, font=(None, 12), fill="white", tag=tag) - canvas.create_text(self.bounding_box()[0][0] + shift_x, self.bounding_box()[1][1] - shift_y, text=food_age, font=(None, 12), fill="black", tag=tag) + center = self.screen() + canvas.create_text(*center, text=food_age, font=(None, 10), fill=text_col, tag=tag) class Arrow(TkSprite): def __init__(self, mesh, req_pos, success, **kwargs): From e792cc7e808aa65f0c6a8f28389d1598d2b58e2a Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Thu, 8 Aug 2024 16:09:38 +0200 Subject: [PATCH 24/28] ENH: Draw bot shadow and remove fill colour of sight radius --- pelita/ui/tk_canvas.py | 156 +++++++++++++++++++++++++++++++++++----- pelita/ui/tk_sprites.py | 2 + 2 files changed, 140 insertions(+), 18 deletions(-) diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index 653119638..abfe63d1a 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -20,7 +20,23 @@ from ..game import next_round_turn from ..team import _ensure_list_tuples -from .tk_sprites import BotSprite, Food, Wall, Arrow, RED, BLUE, YELLOW, GREY, BROWN, LIGHT_BLUE, LIGHT_RED, STRONG_BLUE, STRONG_RED +from .tk_sprites import ( + BotSprite, + Food, + Wall, + Arrow, + RED, + BLUE, + YELLOW, + GREY, + LIGHT_GREY, + SELECTED, + BROWN, + LIGHT_BLUE, + LIGHT_RED, + STRONG_BLUE, + STRONG_RED, +) from .tk_utils import wm_delete_window_handler from .. import layout @@ -439,6 +455,7 @@ def draw_universe(self, game_state): self.draw_grid() self.draw_selected(game_state) self.draw_line_of_sight(game_state) + self.draw_bot_shadow(game_state) self.draw_background() self.draw_maze(game_state) self.draw_food(game_state) @@ -496,6 +513,9 @@ def draw_box(pos): # game has not started yet return + line_col = STRONG_BLUE if bot % 2 == 0 else STRONG_RED + fill_col = LIGHT_BLUE if bot % 2 == 0 else LIGHT_RED + try: old_pos = tuple(game_state['requested_moves'][bot]['previous_position']) except TypeError: @@ -519,23 +539,20 @@ def on_edge(x, y): return x == 0 or x == game_state['shape'][0] - 1 or y == 0 or y == game_state['shape'][1] - 1 - def draw_line(pos, color, loc): + def draw_line(pos, line_col, loc): x0_ = self.mesh_graph.mesh_to_screen_x(pos[0], loc[0]) y0_ = self.mesh_graph.mesh_to_screen_y(pos[1], loc[1]) x1_ = self.mesh_graph.mesh_to_screen_x(pos[0], loc[2]) y1_ = self.mesh_graph.mesh_to_screen_y(pos[1], loc[3]) - self.ui.game_canvas.create_line(x0_, y0_, x1_, y1_, width=2, fill=color, tag=("line_of_sight")) - - team_col = STRONG_BLUE if bot % 2 == 0 else STRONG_RED - + self.ui.game_canvas.create_line(x0_, y0_, x1_, y1_, width=3, fill=line_col, tag=("line_of_sight")) - def draw_box(pos, fill): + def draw_box(pos, fill_col): ul = self.mesh_graph.mesh_to_screen(pos, (-1, -1)) ur = self.mesh_graph.mesh_to_screen(pos, (1, -1)) ll = self.mesh_graph.mesh_to_screen(pos, (-1, 1)) lr = self.mesh_graph.mesh_to_screen(pos, (1, 1)) - self.ui.game_canvas.create_rectangle(*ul, *lr, width=0, fill=fill, tag=("line_of_sight", "area_of_sight")) + self.ui.game_canvas.create_rectangle(*ul, *lr, width=0, fill=fill_col, tag=("line_of_sight", "area_of_sight")) for dx in range(- sight_distance, sight_distance + 1): for dy in range(- sight_distance, sight_distance + 1): @@ -546,30 +563,133 @@ def draw_box(pos, fill): if not in_maze(pos[0], pos[1]): continue - draw_box(pos, fill=LIGHT_BLUE if bot % 2 == 0 else LIGHT_RED) + # Currently not used + # draw_box(pos, fill_col=fill_col) # add edge around cells at the line of sight max if (dx, dy) in border_cells_relative: if dx >= 0: - draw_line(pos, loc=(1, 1, 1, -1), color=team_col) + draw_line(pos, loc=(1, 1, 1, -1), line_col=line_col) if dx <= 0: - draw_line(pos, loc=(-1, 1, -1, -1), color=team_col) + draw_line(pos, loc=(-1, 1, -1, -1), line_col=line_col) if dy >= 0: - draw_line(pos, loc=(1, 1, -1, 1), color=team_col) + draw_line(pos, loc=(1, 1, -1, 1), line_col=line_col) if dy <= 0: - draw_line(pos, loc=(1, -1, -1, -1), color=team_col) + draw_line(pos, loc=(1, -1, -1, -1), line_col=line_col) # add edge around cells at the edge of the maze if on_edge(pos[0], pos[1]): if pos[0] == game_state['shape'][0] - 1: - draw_line(pos, loc=(1, 1, 1, -1), color=team_col) + draw_line(pos, loc=(1, 1, 1, -1), line_col=line_col) if pos[0] == 0: - draw_line(pos, loc=(-1, 1, -1, -1), color=team_col) + draw_line(pos, loc=(-1, 1, -1, -1), line_col=line_col) if pos[1] == game_state['shape'][1] - 1: - draw_line(pos, loc=(1, 1, -1, 1), color=team_col) + draw_line(pos, loc=(1, 1, -1, 1), line_col=line_col) if pos[1] == 0: - draw_line(pos, loc=(1, -1, -1, -1), color=team_col) + draw_line(pos, loc=(1, -1, -1, -1), line_col=line_col) + + self.ui.game_canvas.tag_lower("area_of_sight") + self.ui.game_canvas.tag_raise("wall") + + + def draw_bot_shadow(self, game_state): + self.ui.game_canvas.delete("bot_shadow") + if not self._grid_enabled: + return + + border_col = "#000" + fill_col = LIGHT_GREY + + scale = self.mesh_graph.half_scale_x * 0.1 + + def draw_box(pos): + ul = self.mesh_graph.mesh_to_screen(pos, (-1, -1)) + ur = self.mesh_graph.mesh_to_screen(pos, (1, -1)) + ll = self.mesh_graph.mesh_to_screen(pos, (-1, 1)) + lr = self.mesh_graph.mesh_to_screen(pos, (1, 1)) + + self.ui.game_canvas.create_rectangle(*ul, *lr, width=2, outline='#111', tag=("bot_shadow",)) + + bot = game_state['turn'] + if bot is None: + # game has not started yet + return + + try: + old_pos = tuple(game_state['requested_moves'][bot]['previous_position']) + except TypeError: + old_pos = game_state['bots'][bot] + draw_box(old_pos) + + sight_distance = game_state["shadow_distance"] + # starting from old_pos, iterate over all positions that are up to sight_distance + # steps away and put a border around the fields. + border_cells_relative = set( + (dx, dy) + for dx in range(- sight_distance, sight_distance + 1) + for dy in range(- sight_distance, sight_distance + 1) + if abs(dx) + abs(dy) == sight_distance + ) + + def in_maze(x, y): + return 0 <= x < game_state['shape'][0] and 0 <= y < game_state['shape'][1] + + def on_edge(x, y): + return x == 0 or x == game_state['shape'][0] - 1 or y == 0 or y == game_state['shape'][1] - 1 + + + def draw_line(pos, line_col, loc): + x0_ = self.mesh_graph.mesh_to_screen_x(pos[0], loc[0]) + y0_ = self.mesh_graph.mesh_to_screen_y(pos[1], loc[1]) + x1_ = self.mesh_graph.mesh_to_screen_x(pos[0], loc[2]) + y1_ = self.mesh_graph.mesh_to_screen_y(pos[1], loc[3]) + self.ui.game_canvas.create_line(x0_, y0_, x1_, y1_, width=2, fill=line_col, tag=("bot_shadow")) + + + def draw_box(pos, fill_col): + ul = self.mesh_graph.mesh_to_screen(pos, (-1, -1)) + ur = self.mesh_graph.mesh_to_screen(pos, (1, -1)) + ll = self.mesh_graph.mesh_to_screen(pos, (-1, 1)) + lr = self.mesh_graph.mesh_to_screen(pos, (1, 1)) + + self.ui.game_canvas.create_rectangle(*ul, *lr, width=0, fill=fill_col, tag=("bot_shadow", "bot_shadow_area")) + + for dx in range(- sight_distance, sight_distance + 1): + for dy in range(- sight_distance, sight_distance + 1): + if abs(dx) + abs(dy) > sight_distance: + continue + + pos = (old_pos[0] + dx, old_pos[1] + dy) + if not in_maze(pos[0], pos[1]): + continue + draw_box(pos, fill_col=fill_col) + + # Border around the shadow removed for now + # + # # add edge around cells at the line of sight max + # if (dx, dy) in border_cells_relative: + # if dx >= 0: + # draw_line(pos, loc=(1, 1, 1, -1), line_col=border_col) + # if dx <= 0: + # draw_line(pos, loc=(-1, 1, -1, -1), line_col=border_col) + # if dy >= 0: + # draw_line(pos, loc=(1, 1, -1, 1), line_col=border_col) + # if dy <= 0: + # draw_line(pos, loc=(1, -1, -1, -1), line_col=border_col) + + # # add edge around cells at the edge of the maze + # if on_edge(pos[0], pos[1]): + # if pos[0] == game_state['shape'][0] - 1: + # draw_line(pos, loc=(1, 1, 1, -1), line_col=border_col) + # if pos[0] == 0: + # draw_line(pos, loc=(-1, 1, -1, -1), line_col=border_col) + # if pos[1] == game_state['shape'][1] - 1: + # draw_line(pos, loc=(1, 1, -1, 1), line_col=border_col) + # if pos[1] == 0: + # draw_line(pos, loc=(1, -1, -1, -1), line_col=border_col) + + self.ui.game_canvas.tag_lower("bot_shadow_area") self.ui.game_canvas.tag_lower("area_of_sight") self.ui.game_canvas.tag_raise("wall") @@ -738,7 +858,7 @@ def field_status(pos): ll = self.mesh_graph.mesh_to_screen(self.selected, (-1, 1)) lr = self.mesh_graph.mesh_to_screen(self.selected, (1, 1)) - self.ui.game_canvas.create_rectangle(*ul, *lr, fill='#dddddd', tag=("selected",)) + self.ui.game_canvas.create_rectangle(*ul, *lr, fill=SELECTED, tag=("selected",)) self.ui.game_canvas.tag_lower("selected") else: self.ui.status_selected.config(text="nothing selected") diff --git a/pelita/ui/tk_sprites.py b/pelita/ui/tk_sprites.py index e836ce1ae..90a0f64d5 100644 --- a/pelita/ui/tk_sprites.py +++ b/pelita/ui/tk_sprites.py @@ -19,6 +19,8 @@ def col(red, green, blue): YELLOW = col(242, 255, 83) YELLOW = '#FFE38B' GREY = col(80, 80, 80) +LIGHT_GREY = col(230, 230, 230) +SELECTED = col(200, 200, 200) BROWN = col(48, 26, 22) SHADOW_RED = '#B37373' From 1271bd93283626901dc936fed319d44246e33572 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Thu, 8 Aug 2024 16:24:23 +0200 Subject: [PATCH 25/28] =?UTF-8?q?BF:=20Do=20not=20set=20the=20enemy?= =?UTF-8?q?=E2=80=99s=20shaded=20food?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pelita/team.py | 2 +- test/test_team.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pelita/team.py b/pelita/team.py index 8a1745be0..aa171ad1f 100644 --- a/pelita/team.py +++ b/pelita/team.py @@ -766,7 +766,7 @@ def make_bots(*, walls, shape, initial_positions, homezone, team, enemy, round, is_noisy=enemy['is_noisy'][idx], error_count=enemy['error_count'], food=_ensure_list_tuples(enemy['food']), - shaded_food=_ensure_list_tuples(team['shaded_food']), + shaded_food=[], walls=walls, shape=shape, round=round, diff --git a/test/test_team.py b/test/test_team.py index 1383d0cad..c98b15b13 100644 --- a/test/test_team.py +++ b/test/test_team.py @@ -400,10 +400,16 @@ def asserting_team(bot, state): assert set(bot.homezone) == set(homezones[0]) assert set(bot.enemy[0].homezone) == set(homezones[1]) assert set(bot.shaded_food) == set([(1, 1), (3, 1), (4, 1), (5, 1)]) + assert set(bot.other.shaded_food) == set([(1, 1), (3, 1), (4, 1), (5, 1)]) + assert set(bot.enemy[0].shaded_food) == set() + assert set(bot.enemy[1].shaded_food) == set() else: assert set(bot.homezone) == set(homezones[1]) assert set(bot.enemy[0].homezone) == set(homezones[0]) assert set(bot.shaded_food) == set() + assert set(bot.other.shaded_food) == set() + assert set(bot.enemy[0].shaded_food) == set() + assert set(bot.enemy[1].shaded_food) == set() return bot.position state = run_game([asserting_team, asserting_team], max_rounds=1, layout_dict=parsed) From 1f93a03add37b37ea1e65973c02e2cbc6489f93f Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Thu, 8 Aug 2024 17:06:16 +0200 Subject: [PATCH 26/28] RF: Fine-tune the food-relocation condition --- pelita/gamestate_filters.py | 2 +- test/test_filter_gamestates.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pelita/gamestate_filters.py b/pelita/gamestate_filters.py index 965a641b7..b1621747b 100644 --- a/pelita/gamestate_filters.py +++ b/pelita/gamestate_filters.py @@ -179,7 +179,7 @@ def relocate_expired_food(game_state, team, radius, max_food_age=None): targets = sorted(list(targets)) for pellet in sorted(list(food[team])): # We move the pellet if it is in the food_age dict and exceeds the max_food_age - if food_age[team].get(pellet, 0) >= max_food_age: + if food_age[team].get(pellet, 0) > max_food_age: if not targets: # we have no free positions anymore, just let the food stay where it is # we do not update the age, so this pellet will get a chance to be diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index da35cda20..c24bd4d55 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -742,7 +742,7 @@ def test_relocate_expired_food_nospaceleft(): parsed = parse_layout(test_layout) food = split_food(parsed['shape'][0], parsed['food']) food_age = [{}, {}] - food_age[0][to_relocate] = 1 + food_age[0][to_relocate] = max_age parsed.update({ "food": food, @@ -758,8 +758,8 @@ def test_relocate_expired_food_nospaceleft(): # anywhere assert to_relocate in out['food'][0] assert len(out['food'][0]) == 7 - # check that food age has increased - assert out['food_age'][0] == {to_relocate: 2} + # check that food age has increased over the allowed maximum + assert out['food_age'][0] == {to_relocate: max_age + 1} # now make space for the food and check that it gets located in the free spot parsed.update(out) From 7a5e387189a2ba10f1f5045b37975e2be5b48bee Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Thu, 8 Aug 2024 17:06:43 +0200 Subject: [PATCH 27/28] UI: Do not show the food age --- pelita/ui/tk_canvas.py | 2 +- pelita/ui/tk_sprites.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index abfe63d1a..13e38989c 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -924,7 +924,7 @@ def draw_food(self, game_state): food_age=food_age, max_food_age=max_food_age, ) - food_item.draw(self.ui.game_canvas, show_lifetime=self._grid_enabled) + food_item.draw(self.ui.game_canvas, show_lifetime=False) self.food_items[position] = food_item def draw_maze(self, game_state): diff --git a/pelita/ui/tk_sprites.py b/pelita/ui/tk_sprites.py index 90a0f64d5..a368e0be3 100644 --- a/pelita/ui/tk_sprites.py +++ b/pelita/ui/tk_sprites.py @@ -26,7 +26,7 @@ def col(red, green, blue): SHADOW_RED = '#B37373' SHADOW_BLUE = '#6D92B3' -FOOD_WARNING_TIME = 5 +FOOD_WARNING_TIME = 6 def rotate(arc, rotation): """Helper for rotation normalisation.""" From b0d9656b8fbbd40310fbce0db2bc0ae1e2d4d68d Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Thu, 8 Aug 2024 17:51:53 +0200 Subject: [PATCH 28/28] UI: Do not show shadows for pacman. --- pelita/ui/tk_canvas.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index 13e38989c..f3e844acc 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -619,6 +619,17 @@ def draw_box(pos): old_pos = tuple(game_state['requested_moves'][bot]['previous_position']) except TypeError: old_pos = game_state['bots'][bot] + + boundary = game_state['shape'][0] / 2 + if bot % 2 == 0: + in_homezone = old_pos[0] < boundary + else: + in_homezone = old_pos[0] >= boundary + + if not in_homezone: + # We are a pacman. No shadow + return + draw_box(old_pos) sight_distance = game_state["shadow_distance"]