Skip to content

Commit

Permalink
Merge pull request #811 from Debilski/feature/food-lifetimes
Browse files Browse the repository at this point in the history
Discourage camping: Food relocation and ghost shadow
  • Loading branch information
Debilski authored Aug 8, 2024
2 parents 232ad80 + b0d9656 commit 5845d10
Show file tree
Hide file tree
Showing 10 changed files with 592 additions and 65 deletions.
78 changes: 58 additions & 20 deletions pelita/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
import subprocess
import sys
import time
import math
from warnings import warn

from . import layout
from .exceptions import FatalException, NonFatalException, NoFoodWarning, PlayerTimeout
from .gamestate_filters import noiser
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
Expand All @@ -32,6 +33,11 @@
#: The radius for the uniform noise
NOISE_RADIUS = 5

#: The lifetime of food pellets in a shadow in turns
MAX_FOOD_AGE = 30

#: Food pellet shadow distance
SHADOW_DISTANCE = 3

class TkViewer:
def __init__(self, *, address, controller, geometry=None, delay=None, stop_after=None, fullscreen=False):
Expand Down Expand Up @@ -78,9 +84,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_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):
""" Run a pelita match.
Expand Down Expand Up @@ -182,10 +189,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_camping=allow_camping,
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.
Expand Down Expand Up @@ -258,8 +269,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_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):
""" Generates a game state for the given teams and layout with otherwise default values. """

Expand All @@ -274,7 +286,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']):
Expand All @@ -288,14 +300,8 @@ 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'])
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]
Expand All @@ -317,6 +323,9 @@ def split_food(width, food):
#: Food per team. List of sets of (int, int)
food=food,

#: Food ages per team. Dict of (int, int) to int
food_age=[{}, {}],

### Round/turn information
#: Current bot, int, None
turn=None,
Expand Down Expand Up @@ -356,6 +365,12 @@ def split_food(width, food):
#: 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,
Expand Down Expand Up @@ -549,6 +564,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,
Expand All @@ -559,6 +576,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]
}
Expand All @@ -573,6 +591,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]
}
Expand Down Expand Up @@ -618,7 +637,12 @@ def prepare_viewer_state(game_state):
"""
viewer_state = {}
viewer_state.update(game_state)

# Flatten food and food_age
viewer_state['food'] = list((viewer_state['food'][0] | viewer_state['food'][1]))
# 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.
Expand All @@ -645,6 +669,7 @@ def prepare_viewer_state(game_state):
del viewer_state['rnd']
del viewer_state['viewers']
del viewer_state['controller']

return viewer_state


Expand All @@ -671,6 +696,11 @@ def play_turn(game_state, allow_exceptions=False):
turn = game_state['turn']
round = game_state['round']
team = turn % 2

# 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:
position_dict = request_new_position(game_state)
Expand Down Expand Up @@ -846,7 +876,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]
Expand All @@ -858,7 +888,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]
Expand Down Expand Up @@ -1003,6 +1033,14 @@ def check_exit_remote_teams(game_state):
pass


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


def game_print(turn, msg):
allow_unicode = not _mswindows
if turn % 2 == 0:
Expand Down
90 changes: 85 additions & 5 deletions pelita/gamestate_filters.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 """

Expand Down Expand Up @@ -121,6 +116,91 @@ 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_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_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_age[team]:
food_age[team][pellet] += 1
else:
food_age[team][pellet] = 1
else:
if pellet in food_age[team]:
del food_age[team][pellet]

return {'food_age': food_age}


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_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_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
# - 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])):
# 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):
""" Manhattan distance between two points.
Expand Down
4 changes: 3 additions & 1 deletion pelita/scripts/pelita_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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-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',
Expand Down Expand Up @@ -451,7 +453,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))
Expand Down
4 changes: 4 additions & 0 deletions pelita/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ def __init__(self, *, bot_index,
shape,
homezone,
food,
shaded_food,
score,
kills,
deaths,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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=[],
walls=walls,
shape=shape,
round=round,
Expand Down
Loading

0 comments on commit 5845d10

Please sign in to comment.