Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 134 additions & 130 deletions pelita/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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)
Expand Down Expand Up @@ -294,11 +295,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 = []
Expand Down Expand Up @@ -342,121 +343,121 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, rng=None,

# Initialize the game state.

game_state = dict(
game_state: GameState = {
### 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,

### 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']
}


# Wait until the controller tells us that it is ready
Expand Down Expand Up @@ -659,99 +660,101 @@ 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":
return
raise PelitaIllegalGameState(game_state)

return bot_state
raise PelitaIllegalGameState("Got bad game_state in prepare_bot_state")


def update_viewers(game_state):
Expand Down Expand Up @@ -811,7 +814,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):
""" Plays the next turn of the game.

This function increases the round and turn counters, requests a move
Expand Down Expand Up @@ -936,7 +940,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
Expand Down Expand Up @@ -1210,8 +1214,8 @@ def cleanup_remote_teams(game_state):
team.cleanup()


def split_food(width, food):
team_food = [set(), set()]
def split_food(width, food: list[Pos]):
team_food: tuple[set[Pos], set[Pos]] = (set(), set())
for pos in food:
idx = pos[0] // (width // 2)
team_food[idx].add(pos)
Expand Down
Loading
Loading