diff --git a/pelita/game.py b/pelita/game.py index 5651ae884..07c16e784 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -17,6 +17,7 @@ from .gamestate_filters import noiser, relocate_expired_food, update_food_age, in_homezone from .layout import get_legal_positions, initial_positions from .network import Controller, RemotePlayerFailure, RemotePlayerRecvTimeout, RemotePlayerSendError, ZMQPublisher +from .spec import GameState, Layout, Pos, TeamInitial, TeamState, TeamStateFinished from .team import RemoteTeam, make_team from .viewer import (AsciiViewer, ProgressViewer, ReplayWriter, ReplyToViewer, ResultPrinter) @@ -298,11 +299,11 @@ def setup_viewers(viewers, print_result=True): return viewer_state -def setup_game(team_specs, *, layout_dict, max_rounds=300, rng=None, +def setup_game(team_specs, *, layout_dict: Layout, max_rounds=300, rng=None, allow_camping=False, error_limit=5, timeout_length=TIMEOUT_SECS, initial_timeout_length=INITIAL_TIMEOUT_SECS, viewers=None, store_output=False, team_names=(None, None), team_infos=(None, None), - raise_bot_exceptions=False, print_result=True): + raise_bot_exceptions=False, print_result=True) -> GameState: """ Generates a game state for the given teams and layout with otherwise default values. """ if viewers is None: viewers = [] @@ -346,107 +347,107 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, rng=None, # Initialize the game state. - game_state = dict( + game_state: GameState = { #: UUID - game_uuid=str(uuid.uuid4()), + 'game_uuid': str(uuid.uuid4()), ### The layout attributes #: Walls. Set of (int, int) - walls=set(layout_dict['walls']), + 'walls': set(layout_dict['walls']), #: Shape of the maze. (int, int) - shape=layout_dict['shape'], + 'shape': layout_dict['shape'], #: Food per team. List of sets of (int, int) - food=food, + 'food': food, #: Food ages per team. Dict of (int, int) to int - food_age=[{}, {}], + 'food_age': [{}, {}], ### Round/turn information #: Phase - game_phase='INIT', + 'game_phase': 'INIT', #: Current bot, int, None - turn=None, + 'turn': None, #: Current round, int, None - round=None, + 'round': None, #: Is the game finished? bool - gameover=False, + 'gameover': False, #: Who won? int, None - whowins=None, + 'whowins': None, ### Bot/team status #: Positions of all bots. List of (int, int) - bots=layout_dict['bots'][:], + 'bots': layout_dict['bots'][:], #: Score of the teams. List of int - score=[0] * 2, + 'score': [0, 0], #: Fatal errors - fatal_errors=[[], []], + 'fatal_errors': [[], []], #: Number of timeouts for a team - timeouts=[{}, {}], + 'timeouts': [{}, {}], ### Configuration #: Maximum number of rounds, int - max_rounds=max_rounds, + 'max_rounds': max_rounds, #: Time till timeout, int - timeout_length=timeout_length, + 'timeout_length': timeout_length, #: Initial timeout, int - initial_timeout=initial_timeout_length, + 'initial_timeout': initial_timeout_length, #: Noise radius, int - noise_radius=NOISE_RADIUS, + 'noise_radius': NOISE_RADIUS, #: Sight distance, int - sight_distance=SIGHT_DISTANCE, + 'sight_distance': SIGHT_DISTANCE, #: Max food age - max_food_age=max_food_age, + 'max_food_age': max_food_age, #: Shadow distance, int - shadow_distance=SHADOW_DISTANCE, + 'shadow_distance': SHADOW_DISTANCE, ### Informative #: Name of the teams. Tuple of str - team_names=team_names, + 'team_names': list(team_names), #: Additional team info. Tuple of str|None - team_infos=team_infos, + 'team_infos': list(team_infos), #: Time each team needed, list of float - team_time=[0, 0], + 'team_time': [0.0, 0.0], # List of bot deaths, which counts the number of deaths per bot # In other words, deaths[bot_idx] is the number of times the bot # bot_idx has been killed until now. - deaths = [0]*4, + 'deaths': [0] * 4, # List of bot kills, which counts the number of kills per bot # In other words, kills[bot_idx] is the number of times the bot # bot_idx has killed another bot until now. - kills = [0]*4, + 'kills': [0] * 4, # List of boolean flags weather bot has been eaten since its last move - bot_was_killed = [False]*4, + 'bot_was_killed': [False]*4, # The noisy positions that the bot in `turn` has currently been shown. # None, if not noisy - noisy_positions = [None] * 4, + 'noisy_positions': [None] * 4, #: The moves that the bots returned. Keeps only the recent one at the respective bot’s index. - requested_moves=[None] * 4, + 'requested_moves': [None] * 4, #: Messages the bots say. Keeps only the recent one at the respective bot’s index. - say=[""] * 4, + 'say': [""] * 4, #: List of 4 lists to store the cell overlays for each bot # Items of each list are dictionaries in the form: @@ -455,28 +456,27 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, rng=None, # At the moment only property 'color' is used by the TK-viewer, # but more can be added without modifying the network protocol or # this list - overlays=[[], [], [], []], + 'overlays': [[], [], [], []], ### Internal #: Internal team representation - teams=[None] * 2, + 'teams': [None] * 2, #: Random number generator - rng=rng, + 'rng': rng, #: Error limit. A team loses when the limit is reached, int - error_limit=error_limit, + 'error_limit': error_limit, #: Viewers, list - viewers=viewer_state['viewers'], + 'viewers': viewer_state['viewers'], #: Controller - controller=viewer_state['controller'] - ) + 'controller': viewer_state['controller'] + } _logger.info("Creating game %s", game_state['game_uuid']) - # Wait until the controller tells us that it is ready # We then can send the initial maze # This call *blocks* until the controller replies @@ -677,99 +677,103 @@ def request_new_position(game_state): return bot_reply -def prepare_bot_state(game_state, team_idx=None): +def prepare_bot_state(game_state: GameState, team_idx=None) -> TeamState | TeamInitial | TeamStateFinished: """ Prepares the bot’s game state for the current bot. + NB: This will update the game_state to store new noisy positions. """ - if game_state['game_phase'] == 'INIT': - # We assume that we are in get_initial phase - turn = team_idx - bot_turn = None - seed = game_state['rng'].randint(0, sys.maxsize) - elif game_state['game_phase'] == 'FINISHED': - # Called for remote players in _exit - turn = team_idx - bot_turn = None - seed = None - elif game_state['game_phase'] == 'RUNNING': - turn = game_state['turn'] - bot_turn = game_state['turn'] // 2 - seed = None - else: - _logger.warning("Got bad game_state in prepare_bot_state") - return - - bot_position = game_state['bots'][turn] - own_team = turn % 2 - enemy_team = 1 - own_team - enemy_positions = game_state['bots'][enemy_team::2] - noised_positions = noiser(walls=game_state['walls'], - shape=game_state['shape'], - bot_position=bot_position, - enemy_positions=enemy_positions, - noise_radius=game_state['noise_radius'], - sight_distance=game_state['sight_distance'], - rng=game_state['rng']) - - - # Update noisy_positions in the game_state - # reset positions - game_state['noisy_positions'] = [None] * 4 - noisy_or_none = [ - noisy_pos if is_noisy else None - for is_noisy, noisy_pos in - 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, - 'bot_positions': game_state['bots'][own_team::2], - 'score': game_state['score'][own_team], - 'kills': game_state['kills'][own_team::2], - 'deaths': game_state['deaths'][own_team::2], - 'bot_was_killed': game_state['bot_was_killed'][own_team::2], - 'error_count': len(game_state['timeouts'][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] - } - - enemy_state = { - 'team_index': enemy_team, - 'bot_positions': noised_positions['enemy_positions'], - 'is_noisy': noised_positions['is_noisy'], - 'score': game_state['score'][enemy_team], - 'kills': game_state['kills'][enemy_team::2], - 'deaths': game_state['deaths'][enemy_team::2], - '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] - } - - bot_state = { - 'team': team_state, - 'enemy': enemy_state, - 'round': game_state['round'], - 'bot_turn': bot_turn, - 'timeout_length': game_state['timeout_length'], - 'max_rounds': game_state['max_rounds'], - } + match game_state['game_phase']: + case "INIT": + turn = team_idx + seed = game_state['rng'].randint(0, sys.maxsize) + + team_state_initial: TeamInitial = { + 'walls': game_state['walls'], + 'shape': game_state['shape'], + 'seed': seed, + 'max_rounds': game_state['max_rounds'], + 'team_names': game_state['team_names'][:], + 'timeout_length': game_state['timeout_length'], + } + return team_state_initial + + case "RUNNING": + turn = game_state['turn'] + + bot_position = game_state['bots'][turn] + own_team = turn % 2 + enemy_team = 1 - own_team + enemy_positions = game_state['bots'][enemy_team::2] + noised_positions = noiser(walls=game_state['walls'], + shape=game_state['shape'], + bot_position=bot_position, + enemy_positions=enemy_positions, + noise_radius=game_state['noise_radius'], + sight_distance=game_state['sight_distance'], + rng=game_state['rng']) + + # Update noisy_positions in the game_state + # reset positions + game_state['noisy_positions'] = [None] * 4 + noisy_or_none = [ + noisy_pos if is_noisy else None + for is_noisy, noisy_pos in + zip(noised_positions['is_noisy'], noised_positions['enemy_positions']) + ] + game_state['noisy_positions'][enemy_team::2] = noisy_or_none + + bots = game_state['bots'][:] + bots[enemy_team::2] = noised_positions['enemy_positions'] + + is_noisy = [False for _ in range(4)] + is_noisy[enemy_team::2] = noised_positions['is_noisy'] + + shaded_food_own = list(pos for pos, age in game_state['food_age'][own_team].items() + if age > 0) + shaded_food = [[], []] + shaded_food[own_team] = shaded_food_own + + bot_state: TeamState = { + 'bots': bots, + 'score': game_state['score'][:], + 'kills': game_state['kills'][:], + 'deaths': game_state['deaths'][:], + 'bot_was_killed': game_state['bot_was_killed'][:], + 'error_count': [len(e) for e in game_state['timeouts'][:]], + 'food': [list(team_food) for team_food in game_state['food']], + 'shaded_food': shaded_food, + 'team_time': game_state['team_time'][:], + 'is_noisy': is_noisy, + 'round': game_state['round'], + 'turn': game_state['turn'], + } + return bot_state + + case "FINISHED": + # Called for remote players in _exit + + team_state_final: TeamStateFinished = { + 'bots': game_state['bots'][:], + 'score': game_state['score'][:], + 'kills': game_state['kills'][:], + 'deaths': game_state['deaths'][:], + 'bot_was_killed': game_state['bot_was_killed'][:], + 'error_count': [len(e) for e in game_state['timeouts'][:]], + 'food': [list(team_food) for team_food in game_state['food']], + 'team_time': game_state['team_time'][:], + 'round': game_state['round'], + 'turn': game_state['turn'], + 'whowins': game_state['whowins'] + } + return team_state_final - if game_state['game_phase'] == 'INIT': - bot_state.update({ - 'walls': game_state['walls'], # only in initial round - 'shape': game_state['shape'], # only in initial round - 'seed': seed # only used in set_initial phase - }) + case "FAILURE": + # FIXME + # TODO + return + raise PelitaIllegalGameState(game_state) - return bot_state + raise PelitaIllegalGameState("Got bad game_state in prepare_bot_state") def update_viewers(game_state): @@ -829,7 +833,8 @@ def prepare_viewer_state(game_state): return viewer_state -def play_turn(game_state, raise_bot_exceptions=False): + +def play_turn(game_state: GameState, raise_bot_exceptions=False) -> GameState: """ Plays the next turn of the game. This function increases the round and turn counters, requests a move @@ -962,7 +967,7 @@ def apply_bot_kills(game_state): return state -def apply_move(gamestate, bot_position): +def apply_move(gamestate: GameState, bot_position): """Plays a single step of a bot by applying the game rules to the game state. The rules are: - if the playing team has an error count of >4 or a fatal error they lose - a legal step must not be on a wall, else the error count is increased by 1 and a random move is chosen for the bot @@ -1236,7 +1241,7 @@ def cleanup_remote_teams(game_state): team.cleanup() -def split_food(width, food): +def split_food(width, food: list[Pos]): team_food = [set(), set()] for pos in food: idx = pos[0] // (width // 2) diff --git a/pelita/player/SmartEatingPlayer.py b/pelita/player/SmartEatingPlayer.py index a4df46e1a..fd443725a 100644 --- a/pelita/player/SmartEatingPlayer.py +++ b/pelita/player/SmartEatingPlayer.py @@ -1,8 +1,61 @@ +from pelita.game import apply_move, next_round_turn from pelita.player import food_eating_player +from pelita.spec import FullTeamState +from pelita.team import Bot, _ensure_list_tuples, make_bots + +def simulate_move(bot: Bot, next_pos): + game_state: FullTeamState = dict(bot._game_state) + game_state['bots'] = _ensure_list_tuples(game_state['bots']) + game_state['error_limit'] = 0 + game_state['gameover'] = False + game_state['walls'] = bot.walls + game_state['shape'] = bot.shape + game_state['fatal_errors'] = [[], []] + game_state['errors'] = [[], []] + game_state['game_phase'] = 'RUNNING' + + print(bot) + print(game_state) + game_state = apply_move(game_state, next_pos) + if not game_state['game_phase'] == 'RUNNING': + return None + + game_state.update(next_round_turn(game_state)) + + + for tidx in range(2): + game_state['food'][tidx] = _ensure_list_tuples(game_state['food'][tidx]) + game_state['shaded_food'][tidx] = _ensure_list_tuples(game_state['shaded_food'][tidx]) + + next_bot = make_bots(bot_positions=game_state['bots'], + is_noisy=game_state['is_noisy'], + walls=bot.walls, + shape=bot.shape, + food=game_state['food'], + shaded_food=game_state['shaded_food'], + round=game_state['round'], + turn=game_state['turn'], + score=game_state['score'], + deaths=game_state['deaths'], + kills=game_state['kills'], + bot_was_killed=game_state['bot_was_killed'], + error_count=game_state['error_count'], + initial_positions=[bot._initial_position, bot.other._initial_position, bot._initial_position, bot.other._initial_position], + homezone=[bot.other.homezone, bot.homezone], + team_names=game_state['team_names'], + team_time=game_state['team_time'], + rng="bot._rng", + graph=bot.graph) + + return next_bot + def smart_eating_player(bot, state): + for pos in bot.legal_positions: + print(simulate_move(bot, next_pos=pos)) + # food eating player but won’t do kamikaze (although a sufficiently smart # enemy will be able to kill the bot in its next turn as it doesn’t flee) next_pos = food_eating_player(bot, state) diff --git a/pelita/spec.py b/pelita/spec.py new file mode 100644 index 000000000..ed1d4f682 --- /dev/null +++ b/pelita/spec.py @@ -0,0 +1,91 @@ +from typing import Literal, TypeAlias, TypedDict, Any +from random import Random + +from .team import Team + +Shape: TypeAlias = tuple[int, int] +Pos: TypeAlias = tuple[int, int] +FoodAges: TypeAlias = dict[Pos, int] + +class Layout(TypedDict): + bots: list[Pos] + walls: set[Pos] + shape: Shape + food: list[Pos] + # food: tuple[set[Pos], set[Pos]] + +class GameState(TypedDict): + game_uuid: str + walls: set[Pos] + shape: Shape + food: list[set[Pos]] + food_age: list[FoodAges] + game_phase: Literal["INIT", "RUNNING", "FAILURE", "FINISHED"] + turn: None|int + round: None|int + gameover: bool + whowins: None|int + bots: list[Pos] + score: list[int] + timeouts: list[Any] + fatal_errors: list[list[Any]] + max_rounds: int + timeout_length: int + initial_timeout: int + noise_radius: int + sight_distance: int + max_food_age: float|int + shadow_distance: int + team_names: list[None|str] + team_infos: list[None|str] + team_time: list[float] + deaths: list[int] + kills: list[int] + bot_was_killed: list[bool] + noisy_positions: list[None|Pos] + requested_moves: list[None|Pos] + say: list[str] + overlays: list[list[dict]] + teams: list[None|Team] + rng: Random + error_limit: int + viewers: list[Any] + controller: None|Any + +class TeamInitial(TypedDict): + walls: set[Pos] + shape: Shape + seed: int + max_rounds: int + team_names: list[str] + timeout_length: float + +class TeamState(TypedDict): + bots: list[Pos] + score: list[int] + kills: list[int] + deaths: list[int] + bot_was_killed: list[bool] + error_count: list[int] + food: list[list[Pos]] + shaded_food: list[list[Pos]] + team_time: list[float] + is_noisy: list[bool] + round: int|None + turn: int + +class TeamStateFinished(TypedDict): + bots: list[Pos] + score: list[int] + kills: list[int] + deaths: list[int] + bot_was_killed: list[bool] + error_count: list[bool] + food: list[list[Pos]] + team_time: list[float] + round: int|None + turn: int + whowins: int + +class FullTeamState(TeamInitial, TeamState): + pass diff --git a/pelita/team.py b/pelita/team.py index a8dee66f1..4d277618c 100644 --- a/pelita/team.py +++ b/pelita/team.py @@ -198,7 +198,7 @@ def __init__(self, team_move: typing.Callable[[typing.Any, typing.Any], typing.T self._bot_track = [[], []] - def set_initial(self, team_id, game_state): + def set_initial(self, team_id, initial_state): """ Sets the bot indices for the team and returns the team name. Currently, we do not call _set_initial on the user side. @@ -212,18 +212,26 @@ def set_initial(self, team_id, game_state): # Reset the team state self._state.clear() + self._team_id = team_id + + self._initial_state = {} + self._initial_state.update(initial_state) + # Initialize the random number generator # with the seed that we received from game - self._rng = Random(game_state['seed']) + self._rng = Random(initial_state['seed']) # Reset the bot tracks self._bot_track = [[], []] # Store the walls, which are only transmitted once - self._walls = _ensure_tuple_tuples(game_state['walls']) + self._walls = _ensure_tuple_tuples(initial_state['walls']) # Store the shape, which is only transmitted once - self._shape = tuple(game_state['shape']) + self._shape = tuple(initial_state['shape']) + + self._team_names = tuple(initial_state['team_names']) + self._max_rounds = initial_state['max_rounds'] # Cache the initial positions so that we don’t have to calculate them at each step self._initial_positions = layout.initial_positions(self._walls, self._shape) @@ -252,17 +260,33 @@ def get_move(self, game_state): ------- move : dict """ - me = make_bots(walls=self._walls, + + for tidx in range(2): + game_state['food'][tidx] = _ensure_list_tuples(game_state['food'][tidx]) + game_state['shaded_food'][tidx] = _ensure_list_tuples(game_state['shaded_food'][tidx]) + + me = make_bots(bot_positions=game_state['bots'], + is_noisy=game_state['is_noisy'], + walls=self._walls, shape=self._shape, + food=game_state['food'], + shaded_food=game_state['shaded_food'], + round=game_state['round'], + turn=game_state['turn'], + score=game_state['score'], + deaths=game_state['deaths'], + kills=game_state['kills'], + bot_was_killed=game_state['bot_was_killed'], + error_count=game_state['error_count'], initial_positions=self._initial_positions, homezone=self._homezone, - team=game_state['team'], - enemy=game_state['enemy'], - round=game_state['round'], - bot_turn=game_state['bot_turn'], + team_names=self._team_names, + team_time=game_state['team_time'], rng=self._rng, graph=self._graph) + me._game_state = dict(game_state) + me._game_state.update(self._initial_state) team = me._team for idx, mybot in enumerate(team): @@ -400,17 +424,19 @@ def wait_ready(self, timeout): raise RemotePlayerRecvTimeout("", "") from None def set_initial(self, team_id, game_state): + # TODO: timeout length should be set when object is created timeout_length = game_state['timeout_length'] + self.request_timeout = timeout_length msg_id = self.conn.send_req("set_initial", {"team_id": team_id, - "game_state": game_state}) + "initial_state": game_state}) reply = self.conn.recv_reply(msg_id, timeout_length) # reply should be None return reply def get_move(self, game_state): - timeout_length = game_state['timeout_length'] + timeout_length = self.request_timeout msg_id = self.conn.send_req("get_move", {"game_state": game_state}) reply = self.conn.recv_reply(msg_id, timeout_length) @@ -813,33 +839,6 @@ def paint_background(self, pos, color='#96FF96'): self._overlay.setdefault((x, y), {}).update({'color' : color}) - # def get_direction(self, position): - # """ Return the direction needed to get to the given position. - - # Raises - # ====== - # ValueError - # If the position cannot be reached by a legal move - # """ - # direction = (position[0] - self.position[0], position[1] - self.position[1]) - # if direction not in self.legal_directions: - # raise ValueError("Cannot reach position %s (would have been: %s)." % (position, direction)) - # return direction - - # def get_position(self, direction): - # """ Return the position reached with the given direction - - # Raises - # ====== - # ValueError - # If the direction is not legal. - # """ - # if direction not in self.legal_directions: - # raise ValueError(f"Direction {direction} is not legal.") - # position = (direction[0] + self.position[0], direction[1] + self.position[1]) - # return position - - def _repr_html_(self): """ Jupyter-friendly representation. """ bot = self @@ -930,72 +929,68 @@ def __repr__(self): return f'' -# def __init__(self, *, bot_index, position, initial_position, walls, homezone, food, is_noisy, score, random, round, is_blue): -def make_bots(*, walls, shape, initial_positions, homezone, team, enemy, round, bot_turn, rng, graph): - bots = {} - - team_index = team['team_index'] - enemy_index = enemy['team_index'] - - team_initial_positions = initial_positions[team_index::2] - enemy_initial_positions = initial_positions[enemy_index::2] - - team_bots = [] - for idx, position in enumerate(team['bot_positions']): - b = Bot(bot_index=idx, - is_on_team=True, - score=team['score'], - deaths=team['deaths'][idx], - kills=team['kills'][idx], - was_killed=team['bot_was_killed'][idx], - 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, - bot_turn=bot_turn, - bot_char=BOT_I2N[team_index + idx*2], - random=rng, - graph=graph, - position=team['bot_positions'][idx], - initial_position=team_initial_positions[idx], - is_blue=team_index % 2 == 0, - homezone=homezone[team_index], - team_name=team['name'], - team_time=team['team_time']) - b._bots = bots - team_bots.append(b) - - enemy_bots = [] - for idx, position in enumerate(enemy['bot_positions']): - b = Bot(bot_index=idx, - is_on_team=False, - score=enemy['score'], - kills=enemy['kills'][idx], - deaths=enemy['deaths'][idx], - was_killed=enemy['bot_was_killed'][idx], - 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, - bot_char = BOT_I2N[team_index + idx*2], - random=rng, - graph=graph, - position=enemy['bot_positions'][idx], - initial_position=enemy_initial_positions[idx], - is_blue=enemy_index % 2 == 0, - homezone=homezone[enemy_index], - team_name=enemy['name'], - team_time=enemy['team_time']) - b._bots = bots - enemy_bots.append(b) - - bots['team'] = team_bots - bots['enemy'] = enemy_bots - return team_bots[bot_turn] +def make_bots(*, + bot_positions, + is_noisy, + walls, + shape, + food, + shaded_food, + round, + turn, + score, + deaths, + kills, + bot_was_killed, + initial_positions, + homezone, + team_names, + team_time, + error_count, + rng, + graph): + + team_index = turn % 2 + bot_turn = turn // 2 + enemy_index = 1 - team_index + + bots = [] + bots_dict = {} + + for idx, position in enumerate(bot_positions): + tidx = idx % 2 + b = Bot( + bot_index=idx // 2, + is_on_team=tidx == team_index, + score=score[tidx], + deaths=deaths[idx], + kills=kills[idx], + was_killed=bot_was_killed[idx], + is_noisy=is_noisy[idx], + error_count=error_count[tidx], + food=food[tidx], + shaded_food=shaded_food[tidx], + walls=walls, + shape=shape, + round=round, + bot_turn=bot_turn, + bot_char=BOT_I2N[idx], + random=rng, + graph=graph, + position=bot_positions[idx], + initial_position=initial_positions[idx], + is_blue=tidx % 2 == 0, + homezone=homezone[tidx], + team_name=team_names[tidx], + team_time=team_time[tidx] + ) + b._bots = bots_dict + bots.append(b) + + team_bots = [b for b in bots if b._is_on_team] + enemy_bots = [b for b in bots if not b._is_on_team] + bots_dict['team'] = team_bots + bots_dict['enemy'] = enemy_bots + + return team_bots[bot_turn] diff --git a/pelita/utils.py b/pelita/utils.py index de12379a5..ea62516e5 100644 --- a/pelita/utils.py +++ b/pelita/utils.py @@ -198,6 +198,7 @@ def setup_test_game(*, layout, is_blue=True, round=None, score=None, seed=None, is_noisy_default.update(is_noisy) is_noisy = is_noisy_default + is_noisy_list = [is_noisy["x"], is_noisy["a"], is_noisy["y"], is_noisy["b"]] if layout is None: @@ -205,61 +206,50 @@ def setup_test_game(*, layout, is_blue=True, round=None, score=None, seed=None, else: layout = parse_layout(layout, food=food, bots=bots) + bot_positions = layout['bots'][:] width, height = layout['shape'] food = split_food(width, layout['food']) + food = [list(team_food) for team_food in food] if is_blue: - team_index = 0 - enemy_index = 1 - bot_positions = [layout['bots'][0], layout['bots'][2]] - enemy_positions = [layout['bots'][1], layout['bots'][3]] - is_noisy_enemy = [is_noisy["x"], is_noisy["y"]] + # We only make the first bot of each team controllable, + # therefore the turn is only 0 or 1 + turn = 0 + score = score[:] + shaded_food_list = [list(shaded_food(bot_positions, food[0], radius=SHADOW_DISTANCE)), []] else: - team_index = 1 - enemy_index = 0 - bot_positions = [layout['bots'][1], layout['bots'][3]] - enemy_positions = [layout['bots'][0], layout['bots'][2]] - is_noisy_enemy = [is_noisy["a"], is_noisy["b"]] - - - team = { - 'bot_positions': bot_positions, - 'team_index': team_index, - 'score': score[team_index], - 'kills': [0]*2, - 'deaths': [0]*2, - 'bot_was_killed' : [False]*2, - 'error_count': 0, - 'food': food[team_index], - 'shaded_food': list(shaded_food(bot_positions, food[team_index], radius=SHADOW_DISTANCE)), - 'name': "blue" if is_blue else "red", - 'team_time': 0.0, - } - enemy = { - 'bot_positions': enemy_positions, - 'team_index': enemy_index, - 'score': score[enemy_index], - 'kills': [0]*2, - 'deaths': [0]*2, - '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, - } - - bot = make_bots(walls=layout['walls'], - shape=layout['shape'], - initial_positions=initial_positions(layout['walls'], layout['shape']), - homezone=create_homezones(layout['shape'], layout['walls']), - team=team, - enemy=enemy, + turn = 1 + score = list(reversed(score)) + shaded_food_list = [[], list(shaded_food(bot_positions, food[1], radius=SHADOW_DISTANCE))] + + + shape = layout['shape'] + walls = layout['walls'] + shape = layout['shape'] + graph = walls_to_graph(layout['walls']) + initial_positions_list = initial_positions(layout['walls'], layout['shape']) + homezone = create_homezones(layout['shape'], layout['walls']) + + bot = make_bots(bot_positions=bot_positions, + is_noisy=is_noisy_list, + walls=walls, + shape=shape, + food=food, + shaded_food=shaded_food_list, round=round, - bot_turn=0, + turn=turn, + score=score, + deaths=[0] * 4, + kills=[0] * 4, + bot_was_killed=[False] * 4, + error_count=[0] * 2, + initial_positions=initial_positions_list, + homezone=homezone, + team_names=["blue", "red"], + team_time=[0.0] * 2, rng=rng, - graph=walls_to_graph(layout['walls'])) + graph=graph) + return bot diff --git a/test/test_filter_gamestates.py b/test/test_filter_gamestates.py index 2f9c4dbac..3794169c9 100644 --- a/test/test_filter_gamestates.py +++ b/test/test_filter_gamestates.py @@ -101,30 +101,31 @@ def test_noiser_no_negative_coordinates(bot_id): assert test_1 and test_2 -def test_noiser_noising_odd_turn1(): +@pytest.mark.parametrize("turn", [0, 2]) +def test_noiser_noising_even_turn(turn): - """ It is the odd team's turn, and the noiser should - work on even bots """ + """ It is the even team's turn, and the noiser should + work on odd bots """ - test_collect_bot0 = [] - test_collect_bot2 = [] + test_collect_bot1 = [] + test_collect_bot3 = [] for ii in range(10): # we let it run 10 times because it could return the original position, # but in most cases it should return a different pos (due to noise) gamestate = make_gamestate() - gamestate["turn"] = 1 + gamestate["turn"] = turn old_bots = gamestate["bots"] new_gamestate = prepare_bot_state(gamestate) - new_bots = new_gamestate['enemy']['bot_positions'] - test_collect_bot0.append((not old_bots[0] == new_bots[0])) - test_collect_bot2.append((not old_bots[2] == new_bots[1])) + new_bots = new_gamestate['bots'] + test_collect_bot1.append((not old_bots[1] == new_bots[1])) + test_collect_bot3.append((not old_bots[3] == new_bots[3])) - assert any(test_collect_bot0) or any(test_collect_bot2) + assert any(test_collect_bot1) and any(test_collect_bot3) -def test_noiser_noising_odd_turn3(): - +@pytest.mark.parametrize("turn", [1, 3]) +def test_noiser_noising_odd_turn(turn): """ It is the odd team's turn, and the noiser should work on even bots """ @@ -135,123 +136,46 @@ def test_noiser_noising_odd_turn3(): # we let it run 10 times because it could return the original position, # but in most cases it should return a different pos (due to noise) gamestate = make_gamestate() - gamestate["turn"] = 3 + gamestate["turn"] = turn old_bots = gamestate["bots"] new_gamestate = prepare_bot_state(gamestate) - new_bots = new_gamestate['enemy']['bot_positions'] + new_bots = new_gamestate['bots'] test_collect_bot0.append((not old_bots[0] == new_bots[0])) - test_collect_bot2.append((not old_bots[2] == new_bots[1])) - - assert any(test_collect_bot0) or any(test_collect_bot2) - - -def test_noiser_noising_even_turn0(): - - """ It is the even team's turn, and the noiser should - work on odd bots """ - - test_collect_bot1 = [] - test_collect_bot3 = [] - for ii in range(10): - - # we let it run 10 times because it could return the original position, - # but in most cases it should return a different pos (due to noise) - gamestate = make_gamestate() - gamestate["turn"] = 0 - old_bots = gamestate["bots"] - new_gamestate = prepare_bot_state(gamestate) - new_bots = new_gamestate['enemy']['bot_positions'] - test_collect_bot1.append((not old_bots[1] == new_bots[0])) - test_collect_bot3.append((not old_bots[3] == new_bots[1])) - - assert any(test_collect_bot1) or any(test_collect_bot3) - - -def test_noiser_noising_even_turn2(): - - """ It is the even team's turn, and the noiser should - work on odd bots """ - - test_collect_bot1 = [] - test_collect_bot3 = [] - for ii in range(10): - - # we let it run 10 times because it could return the original position, - # but in most cases it should return a different pos (due to noise) - gamestate = make_gamestate() - gamestate["turn"] = 2 - old_bots = gamestate["bots"] - new_gamestate = prepare_bot_state(gamestate) - new_bots = new_gamestate['enemy']['bot_positions'] - test_collect_bot1.append((not old_bots[1] == new_bots[0])) - test_collect_bot3.append((not old_bots[3] == new_bots[1])) - - assert any(test_collect_bot1) or any(test_collect_bot3) - - -def test_noiser_not_noising_own_team_even0(): - - """ It is the even team's turn, and the noiser should - not work on own bots """ - - gamestate = make_gamestate() - gamestate["turn"] = 0 - old_bots = gamestate["bots"] - new_gamestate = prepare_bot_state(gamestate) - new_bots = new_gamestate['team']['bot_positions'] - test_bot0 = old_bots[0] == new_bots[0] - test_bot2 = old_bots[2] == new_bots[1] - - assert test_bot0 or test_bot2 + test_collect_bot2.append((not old_bots[2] == new_bots[2])) + assert any(test_collect_bot0) and any(test_collect_bot2) -def test_noiser_not_noising_own_team_even2(): +@pytest.mark.parametrize("turn", [0, 2]) +def test_noiser_not_noising_own_team_even_turn(turn): """ It is the even team's turn, and the noiser should not work on own bots """ gamestate = make_gamestate() - gamestate["turn"] = 2 + gamestate["turn"] = turn old_bots = gamestate["bots"] new_gamestate = prepare_bot_state(gamestate) - new_bots = new_gamestate['team']['bot_positions'] + new_bots = new_gamestate['bots'] test_bot0 = old_bots[0] == new_bots[0] - test_bot2 = old_bots[2] == new_bots[1] + test_bot2 = old_bots[2] == new_bots[2] - assert test_bot0 or test_bot2 + assert test_bot0 and test_bot2 - -def test_noiser_not_noising_own_team_odd1(): +@pytest.mark.parametrize("turn", [1, 3]) +def test_noiser_not_noising_own_team_odd_turn(turn): """ It is the odd team's turn, and the noiser should not work on own bots """ gamestate = make_gamestate() - gamestate["turn"] = 1 + gamestate["turn"] = turn old_bots = gamestate["bots"] new_gamestate = prepare_bot_state(gamestate) - new_bots = new_gamestate['team']['bot_positions'] - test_bot1 = old_bots[1] == new_bots[0] - test_bot3 = old_bots[3] == new_bots[1] - - assert test_bot1 or test_bot3 - - -def test_noiser_not_noising_own_team_odd3(): - - """ It is the odd team's turn, and the noiser should - not work on own bots """ - - gamestate = make_gamestate() - gamestate["turn"] = 3 - old_bots = gamestate["bots"] - new_gamestate = prepare_bot_state(gamestate) - new_bots = new_gamestate['team']['bot_positions'] - test_bot1 = old_bots[1] == new_bots[0] - test_bot3 = old_bots[3] == new_bots[1] - - assert test_bot1 or test_bot3 + new_bots = new_gamestate['bots'] + test_bot1 = old_bots[1] == new_bots[1] + test_bot3 = old_bots[3] == new_bots[3] + assert test_bot1 and test_bot3 def test_noiser_not_noising_at_noise_radius0(): @@ -264,20 +188,9 @@ def test_noiser_not_noising_at_noise_radius0(): gamestate["turn"] = tt gamestate["noise_radius"] = 0 new_gamestate = prepare_bot_state(gamestate) - new_team_bots = new_gamestate['team']['bot_positions'] - new_enemy_bots = new_gamestate['enemy']['bot_positions'] - if tt % 2 == 0: - # team 0 - assert old_bots[0] == new_team_bots[0] - assert old_bots[1] == new_enemy_bots[0] - assert old_bots[2] == new_team_bots[1] - assert old_bots[3] == new_enemy_bots[1] - else: - # team 1 - assert old_bots[0] == new_enemy_bots[0] - assert old_bots[1] == new_team_bots[0] - assert old_bots[2] == new_enemy_bots[1] - assert old_bots[3] == new_team_bots[1] + new_bots = new_gamestate['bots'] + + assert old_bots == new_bots @pytest.mark.parametrize("ii", range(30)) diff --git a/test/test_game.py b/test/test_game.py index 65760c036..33b9ede6b 100644 --- a/test/test_game.py +++ b/test/test_game.py @@ -1010,7 +1010,6 @@ def move(b, s): def test_non_existing_file(): l = maze_generator.generate_maze() res = run_game(["blah", "nothing"], max_rounds=1, layout_dict=l) - print(res['fatal_errors']) # We might only catch only one of the errors assert len(res['fatal_errors'][0]) > 0 or len(res['fatal_errors'][1]) > 0 @@ -1185,7 +1184,7 @@ def test_apply_move_resets_bot_was_killed(game_state, bot_to_move, bot_was_kille bot_state = game.prepare_bot_state(game_state) # bot state should have proper bot_was_killed flag - assert bot_state['team']['bot_was_killed'] == bot_was_killed_flags[team_id::2] + assert bot_state['bot_was_killed'] == bot_was_killed_flags # apply a dummy move that should reset bot_was_killed for the current bot _new_test_state = game.apply_move(game_state, current_bot_position) diff --git a/test/test_network.py b/test/test_network.py index 2a3fd83f8..bdf58e4f7 100644 --- a/test/test_network.py +++ b/test/test_network.py @@ -59,14 +59,17 @@ def stopping(bot, state): '__action__': "set_initial", '__data__': { 'team_id': 0, - 'game_state': { + 'initial_state': { 'seed': 0, 'walls': [(0, 0), (1, 0), (2, 0), (3, 0), (0, 1), (3, 1), (0, 2), (3, 2), (0, 3), (1, 3), (2, 3), (3, 3), ], - 'shape': (4, 4) + 'shape': (4, 4), + 'max_rounds': 5, + 'team_names': ['unknown', 'team'], + 'timeout_length': 3 } } } @@ -84,35 +87,23 @@ def stopping(bot, state): '__action__': "get_move", '__data__': { 'game_state': { - 'team': { - 'team_index': 0, - 'bot_positions': [(1, 1), (1, 1)], - 'score': 0, - 'kills': [0]*2, - 'deaths': [0]*2, - 'bot_was_killed': [False]*2, - 'error_count': 0, - 'food': [(1, 1)], - 'shaded_food': [(1, 1)], - 'name': 'dummy', - 'team_time': 0, - }, - 'enemy': { - 'team_index': 1, - 'bot_positions': [(2, 2), (2, 2)], - 'score': 0, - 'kills': [0]*2, - 'deaths': [0]*2, - 'bot_was_killed': [False]*2, - 'food': [(2, 2)], - 'shaded_food': [], - 'name': 'other dummy', - 'team_time': 0, - 'is_noisy': [False, False], - 'error_count': 0 - }, + 'team_index': 0, + 'bots': [(1, 1), (2, 2), (1, 1), (2, 2)], + 'score': [0, 0], + 'kills': [0]*4, + 'deaths': [0]*4, + 'bot_was_killed': [False]*4, + 'error_count': [0, 0], + 'food': [[(1, 1)], [(2, 2)]], + 'shaded_food': [[(1, 1)], []], + 'team_names': ['dummy', 'other_dummy'], + 'team_time': [0, 0], + 'is_noisy': [False, False, False, False], + 'error_count': [0, 0], 'round': 1, - 'bot_turn': 0, + 'turn': 0, + 'timeout_length': 3, + 'max_rounds': 300, } } } @@ -168,8 +159,7 @@ def dealer_good(q, *, num_requests, timeout): if action == 'exit': return assert set_initial['__action__'] == "set_initial" - - current_pos = game_state['__data__']['game_state']['team']['bot_positions'][game_state['__data__']['game_state']['bot_turn']] + current_pos = game_state['__data__']['game_state']['bots'][game_state['__data__']['game_state']['turn']] sock.send_json({'__uuid__': msg_id, '__return__': {'move': current_pos}}) _available_socks = poll.poll(timeout=timeout) @@ -225,7 +215,7 @@ def dealer_bad(q, *, team_name=None, num_requests, checkpoint, timeout): if action == 'exit': return - current_pos = game_state['__data__']['game_state']['team']['bot_positions'][game_state['__data__']['game_state']['bot_turn']] + current_pos = game_state['__data__']['game_state']['bots'][game_state['__data__']['game_state']['turn']] if checkpoint == 5: sock.send_string("No json") return @@ -342,9 +332,14 @@ def test_client_broken(zmq_context, checkpoint): case 0|6|11: assert game_state['game_phase'] == 'FINISHED' assert game_state['whowins'] == 2 + # we should also ensure that there was no timeouts in these cases + # but if we check for uncaught exceptions in the player (below) + # then we also get a location where something went wrong + # assert game_state['timeouts'] == [{}, {}] case 5|7|8|9|10: assert game_state['game_phase'] == 'FINISHED' assert game_state['whowins'] == 0 + # assert game_state['timeouts'] == [{}, {}] case 1|2|3|4: assert game_state['game_phase'] == 'FAILURE'